UUID Comparison Guide for MemberJunction
The Problem: UUID Case Sensitivity Across Database Platforms
Section titled “The Problem: UUID Case Sensitivity Across Database Platforms”MemberJunction supports both SQL Server and PostgreSQL as database backends. These platforms handle UUID casing differently:
| Database | UUID Return Format | Example |
|---|---|---|
| SQL Server | UPPERCASE | A1B2C3D4-E5F6-7890-ABCD-EF1234567890 |
| PostgreSQL | lowercase | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
Both formats represent the same UUID and are semantically identical. However, JavaScript’s === operator performs a case-sensitive string comparison, which means:
// This FAILS when comparing UUIDs from different database platforms'A1B2C3D4-E5F6-7890-ABCD-EF1234567890' === 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'// Result: false (WRONG!)This breaks any code that uses === or !== to compare UUID values — lookups fail silently, filters return empty results, and data appears missing.
The Solution: UUIDsEqual() and NormalizeUUID()
Section titled “The Solution: UUIDsEqual() and NormalizeUUID()”MemberJunction provides two utility functions in @memberjunction/global that handle cross-platform UUID comparison correctly:
UUIDsEqual(uuid1, uuid2): boolean
Section titled “UUIDsEqual(uuid1, uuid2): boolean”Case-insensitive comparison of two UUID strings. Handles null/undefined gracefully.
import { UUIDsEqual } from '@memberjunction/global';
// Cross-platform comparison works correctlyUUIDsEqual('A1B2C3D4-E5F6-7890-ABCD-EF1234567890', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890') // true
// Null safetyUUIDsEqual(null, null) // trueUUIDsEqual(null, 'some-id') // falseNormalizeUUID(uuid): string
Section titled “NormalizeUUID(uuid): string”Normalizes a UUID to lowercase for use in Set, Map, or other data structures that need consistent key formatting.
import { NormalizeUUID } from '@memberjunction/global';
NormalizeUUID('A1B2C3D4-E5F6-7890-ABCD-EF1234567890')// Returns: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
NormalizeUUID(null) // Returns: ''Usage Patterns
Section titled “Usage Patterns”Pattern 1: Finding an item by ID (.find())
Section titled “Pattern 1: Finding an item by ID (.find())”// WRONG - breaks with cross-platform UUIDsconst server = this._Servers.find(s => s.ID === serverId);
// CORRECTimport { UUIDsEqual } from '@memberjunction/global';const server = this._Servers.find(s => UUIDsEqual(s.ID, serverId));Pattern 2: Filtering by a foreign key (.filter())
Section titled “Pattern 2: Filtering by a foreign key (.filter())”// WRONGconst userApps = allUserApps.filter(ua => ua.UserID === userId);
// CORRECTconst userApps = allUserApps.filter(ua => UUIDsEqual(ua.UserID, userId));Pattern 3: Checking existence (.some())
Section titled “Pattern 3: Checking existence (.some())”// WRONGconst exists = items.some(item => item.ID === targetId);
// CORRECTconst exists = items.some(item => UUIDsEqual(item.ID, targetId));Pattern 4: Negated comparison (!==)
Section titled “Pattern 4: Negated comparison (!==)”// WRONGconst others = items.filter(item => item.ID !== excludeId);
// CORRECTconst others = items.filter(item => !UUIDsEqual(item.ID, excludeId));Pattern 5: Sets and maps of UUIDs — UUIDSet / UUIDMap
Section titled “Pattern 5: Sets and maps of UUIDs — UUIDSet / UUIDMap”A native Set or Map compares keys with ===, so an upper-case ID never matches its lower-case twin. Use UUIDSet and UUIDMap: they normalize on every insert and every lookup, so there is no second side to forget.
import { UUIDSet, UUIDMap } from '@memberjunction/global';
// WRONG - Set uses === internallyconst idSet = new Set(items.map(i => i.ID));idSet.has(lookupId); // fails if cases differ
// CORRECTconst idSet = new UUIDSet(items.map(i => i.ID));idSet.Has(lookupId); // any casing, surrounding whitespace ignored
const roleNames = new UUIDMap(roles.map(r => [r.ID, r.Name]));roleNames.Get(permission.RoleID);A hand-built new Set(ids.map(id => NormalizeUUID(id))) also works if every lookup uses NormalizeUUID too. The types exist so nobody has to remember that. Both follow NormalizeUUID exactly, including null/undefined normalizing to ''.
Pattern 6: Angular template bindings
Section titled “Pattern 6: Angular template bindings”In Angular inline templates, you cannot call imported functions directly. Add helper methods to the component class:
// In the component class:import { UUIDsEqual } from '@memberjunction/global';
export class MyComponent { selectedId: string | null = null;
IsSelected(itemId: string): boolean { return this.selectedId != null && UUIDsEqual(this.selectedId, itemId); }}
// In the template:// WRONG: [class.selected]="item.ID === selectedId"// CORRECT: [class.selected]="IsSelected(item.ID)"Pattern 7: Conditional assignment / toggle
Section titled “Pattern 7: Conditional assignment / toggle”// WRONGthis.selectedId = this.selectedId === itemId ? null : itemId;
// CORRECTthis.selectedId = (this.selectedId != null && UUIDsEqual(this.selectedId, itemId)) ? null : itemId;Pattern 8: Matching one list against another — use the set-based helpers
Section titled “Pattern 8: Matching one list against another — use the set-based helpers”UUIDsEqual() compares two IDs, so matching a list against a list with it means a nested scan: n × m comparisons. That’s invisible with five items and pegs a CPU with a few hundred, especially in code that re-runs on every render (an Angular getter or a template-bound method). A single top-level .find() (Patterns 1–3) is fine.
Every nested scan is one of five operations, and each has a helper that does it in one linear pass:
| You want | Use |
|---|---|
| Items whose ID is in a list | FilterByUUIDs(items, ids) |
| Items whose ID is not in a list | ExcludeByUUIDs(items, ids) |
| How many items share each ID | CountByUUID(items).Get(id) ?? 0 |
| The items that share each ID | GroupByUUID(items, x => x.ParentID).Get(id) ?? [] (each group in items order) |
| The item for an ID, looked up repeatedly | IndexByUUID(items).Get(id) (the first item wins, like find) |
import { FilterByUUIDs, ExcludeByUUIDs, CountByUUID, GroupByUUID, IndexByUUID } from '@memberjunction/global';
// SLOW - n x m comparisonsconst picked = roles.filter(r => selectedIds.some(id => UUIDsEqual(id, r.ID)));
// CORRECT - linearconst picked = FilterByUUIDs(roles, selectedIds);
// A different ID field: pass a selector as the third argumentconst stillLoading = ExcludeByUUIDs(pending, loadedIds, p => p.artifactId);const perEntity = CountByUUID(relationships, r => r.RelatedEntityID);const rolesByUser = GroupByUUID(userRoles, r => r.UserID);Every helper keeps the items’ own order, accepts IDs in any casing, and takes an existing UUIDSet in place of the ID list. The selector defaults to item => item.ID.
When You Do NOT Need UUIDsEqual()
Section titled “When You Do NOT Need UUIDsEqual()”Not every .ID === comparison involves UUIDs. These patterns are safe with ===:
- Numeric IDs: Integer primary keys (
numbertype) are not affected - String literal comparisons:
status === 'Active',type === 'Report' - Comparing to
null/undefined:item.ID == nullis fine - Within-transaction comparisons: If both values come from the same query result and haven’t been cached, they’ll have the same case
Why This Matters
Section titled “Why This Matters”Even if you’re currently developing against only one database platform, using UUIDsEqual() is important because:
-
Future-proofing: MemberJunction is designed to support multiple database backends. Code that works on SQL Server today should work on PostgreSQL tomorrow without changes.
-
Cached data mixing: GraphQL data providers cache entity metadata. If a user switches backends behind the same endpoint, stale cached data with one case can be compared against fresh data with the opposite case.
-
External integrations: UUIDs may arrive from external systems (APIs, webhooks, MCP servers) in any case format. Robust comparison prevents silent failures.
-
Silent failures are costly: A
===comparison that returnsfalsefor matching UUIDs doesn’t throw an error — it silently returns no results. These bugs are extremely difficult to diagnose in production.
Automated Enforcement
Section titled “Automated Enforcement”Static Analysis Test
Section titled “Static Analysis Test”packages/MJGlobal/src/__tests__/UUIDCompliance.test.ts scans all packages/ source and fails on:
- Direct comparisons —
.ID ===/!==/.includes(x.ID)patterns that should useUUIDsEqual()or aUUIDSet. A legitimate non-UUID comparison (e.g. a numeric ID) goes inKNOWN_EXCEPTIONS. - Nested scans — a
.some/.every/.find/.findIndex/.filtercallingUUIDsEqualinside the callback of another array iteration or inside a loop (Pattern 8). Detection uses the TypeScript AST, so chaining (list.filter(…).find(…)) is not flagged. It only sees a directUUIDsEqualcall: a scan wrapped in a helper function and called from inside the iteration (for examplerows.filter(r => isChild(r)), whereisChildruns.some(UUIDsEqual)) is not detected, so review such helpers by hand. Sites that predate the check are recorded per file inNESTED_SCAN_BASELINE, which is a ratchet: a file may not gain sites, and when you fix one the check makes you lower the number.
It runs uncached in the Source guards CI job. To run it locally:
pnpm --filter @memberjunction/global exec vitest run --config ../../vitest.shared.ts --passWithNoTests=false src/__tests__/UUIDCompliance.test.tsCross-Database Integration Tests
Section titled “Cross-Database Integration Tests”The file packages/MJCoreEntities/src/__tests__/UUIDCrossDbCompliance.test.ts verifies that engine lookup methods (e.g., GetServerById, GetKeyByID, GetAccountById) work correctly with mixed-case UUIDs. Each test stores data with one case and looks it up with the opposite case, simulating a cross-database scenario.
Quick Reference
Section titled “Quick Reference”| Instead of… | Use… |
|---|---|
a.ID === b.ID | UUIDsEqual(a.ID, b.ID) |
a.ID !== b.ID | !UUIDsEqual(a.ID, b.ID) |
items.find(x => x.ID === id) | items.find(x => UUIDsEqual(x.ID, id)) |
items.filter(x => x.UserID === uid) | items.filter(x => UUIDsEqual(x.UserID, uid)) |
items.some(x => x.ID === id) | items.some(x => UUIDsEqual(x.ID, id)) |
new Set(ids); set.has(id) | new UUIDSet(ids); set.Has(id) |
Template: item.ID === selectedId | Add component method using UUIDsEqual() |
a.filter(x => b.some(y => UUIDsEqual(x.ID, y))) | FilterByUUIDs(a, b) |
a.filter(x => !b.some(y => UUIDsEqual(x.ID, y))) | ExcludeByUUIDs(a, b) |
for (…) { list.find(x => UUIDsEqual(x.ID, id)) } | const byID = IndexByUUID(list); then byID.Get(id) |
for (…) { list.filter(x => UUIDsEqual(x.ParentID, id)) } | const byParent = GroupByUUID(list, x => x.ParentID); then byParent.Get(id) ?? [] |
Import
Section titled “Import”Everything is exported from @memberjunction/global:
import { UUIDsEqual, NormalizeUUID, UUIDSet, UUIDMap, FilterByUUIDs, ExcludeByUUIDs, CountByUUID, GroupByUUID, IndexByUUID } from '@memberjunction/global';Sources: packages/MJGlobal/src/util/UUIDUtils.ts, packages/MJGlobal/src/util/UUIDCollections.ts