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: Set/Map operations
Section titled “Pattern 5: Set/Map operations”Set.has() and Map.get() use strict equality internally, so you must normalize UUIDs before inserting or looking up:
import { NormalizeUUID, UUIDsEqual } from '@memberjunction/global';
// WRONG - Set uses === internallyconst idSet = new Set(items.map(i => i.ID));idSet.has(lookupId); // fails if cases differ
// CORRECT - Normalize on insert AND lookupconst idSet = new Set(items.map(i => NormalizeUUID(i.ID)));idSet.has(NormalizeUUID(lookupId)); // always worksPattern 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;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”The file packages/MJGlobal/src/__tests__/UUIDCompliance.test.ts contains a static analysis test that scans all packages/ source files for remaining .ID === patterns that should use UUIDsEqual(). Run it with:
cd packages/MJGlobal && npx vitest runIf you have a legitimate .ID === comparison that doesn’t involve UUIDs (e.g., a numeric ID), add it to the KNOWN_EXCEPTIONS map in that test file.
Cross-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 Set(ids.map(NormalizeUUID)); set.has(NormalizeUUID(id)) |
Template: item.ID === selectedId | Add component method using UUIDsEqual() |
Import
Section titled “Import”Both functions are exported from @memberjunction/global:
import { UUIDsEqual, NormalizeUUID } from '@memberjunction/global';