@eristack/entity-id
Version:
UUID v7 entity identifiers — sortable, parseable, Drizzle column helper
1 lines • 10.1 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/core/errors.ts","../src/core/types.ts","../src/core/internal.ts","../src/core/entity-id.ts"],"sourcesContent":["export {\n compareEntityIds,\n ENTITY_ID_PARSE_CODE,\n ENTITY_ID_VERSION,\n EntityIdParseError,\n entityIdEquals,\n entityIdToDate,\n entityIdToUnixMs,\n entityIdFactory,\n generateEntityId,\n generateEntityIdAt,\n isValidEntityId,\n MAX_ENTITY_ID_UNIX_MS,\n normalizeEntityId,\n parseEntityId,\n} from \"./core/index.js\";\nexport type { EntityId } from \"./core/index.js\";\n","export const ENTITY_ID_PARSE_CODE = \"ENTITY_ID_PARSE\" as const;\n\nexport class EntityIdParseError extends Error {\n readonly code = ENTITY_ID_PARSE_CODE;\n\n constructor(message: string) {\n super(message);\n this.name = \"EntityIdParseError\";\n }\n}\n","/** Canonical lowercase UUID string with hyphens (RFC 9562 UUID v7). */\nexport type EntityId = string & { readonly __brand: unique symbol };\n\nexport const ENTITY_ID_VERSION = 7 as const;\n\nexport const MAX_ENTITY_ID_UNIX_MS = 0xffff_ffff_ffff;\n","import { EntityIdParseError } from \"./errors.js\";\nimport type { EntityId } from \"./types.js\";\nimport { ENTITY_ID_VERSION, MAX_ENTITY_ID_UNIX_MS } from \"./types.js\";\n\nconst HYPHENATED =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\n\nexport function bytesToCanonicalUuid(bytes: Uint8Array): EntityId {\n const hex = [...bytes].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n const canonical =\n `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n return canonical as EntityId;\n}\n\nexport function canonicalUuidToBytes(id: string): Uint8Array {\n const compact = id.replace(/-/g, \"\").toLowerCase();\n if (compact.length !== 32 || !/^[0-9a-f]+$/.test(compact)) {\n throw new EntityIdParseError(\"Entity id must be 32 hex digits (optional hyphens)\");\n }\n const bytes = new Uint8Array(16);\n for (let i = 0; i < 16; i++) {\n bytes[i] = Number.parseInt(compact.slice(i * 2, i * 2 + 2), 16);\n }\n return bytes;\n}\n\nexport function assertEntityIdV7(bytes: Uint8Array): void {\n const version = (bytes[6]! >> 4) & 0x0f;\n if (version !== ENTITY_ID_VERSION) {\n throw new EntityIdParseError(\n `Entity id must be UUID version ${ENTITY_ID_VERSION}, got version ${version}`,\n );\n }\n const variant = (bytes[8]! >> 6) & 0x03;\n if (variant !== 0b10) {\n throw new EntityIdParseError(\"Entity id must use RFC 4122 variant (10xxxxxx)\");\n }\n}\n\nexport function unixMsFromV7Bytes(bytes: Uint8Array): number {\n const ms =\n (BigInt(bytes[0]!) << 40n) |\n (BigInt(bytes[1]!) << 32n) |\n (BigInt(bytes[2]!) << 24n) |\n (BigInt(bytes[3]!) << 16n) |\n (BigInt(bytes[4]!) << 8n) |\n BigInt(bytes[5]!);\n return Number(ms);\n}\n\nexport function generateEntityIdBytes(\n unixMs: number,\n random: Uint8Array,\n): Uint8Array {\n if (!Number.isFinite(unixMs) || unixMs < 0 || unixMs > MAX_ENTITY_ID_UNIX_MS) {\n throw new EntityIdParseError(\n `Unix timestamp ms out of range for UUID v7 (0..${MAX_ENTITY_ID_UNIX_MS})`,\n );\n }\n if (random.length < 10) {\n throw new EntityIdParseError(\"Random bytes must be at least 10 octets\");\n }\n\n const ts = BigInt(Math.trunc(unixMs));\n const bytes = new Uint8Array(16);\n bytes[0] = Number((ts >> 40n) & 0xffn);\n bytes[1] = Number((ts >> 32n) & 0xffn);\n bytes[2] = Number((ts >> 24n) & 0xffn);\n bytes[3] = Number((ts >> 16n) & 0xffn);\n bytes[4] = Number((ts >> 8n) & 0xffn);\n bytes[5] = Number(ts & 0xffn);\n\n bytes[6] = (random[0]! & 0x0f) | 0x70;\n bytes[7] = random[1]!;\n bytes[8] = (random[2]! & 0x3f) | 0x80;\n bytes[9] = random[3]!;\n bytes[10] = random[4]!;\n bytes[11] = random[5]!;\n bytes[12] = random[6]!;\n bytes[13] = random[7]!;\n bytes[14] = random[8]!;\n bytes[15] = random[9]!;\n\n return bytes;\n}\n\nexport function isHyphenatedUuidLower(value: string): boolean {\n return HYPHENATED.test(value);\n}\n","import { EntityIdParseError } from \"./errors.js\";\nimport {\n assertEntityIdV7,\n bytesToCanonicalUuid,\n canonicalUuidToBytes,\n generateEntityIdBytes,\n isHyphenatedUuidLower,\n unixMsFromV7Bytes,\n} from \"./internal.js\";\nimport type { EntityId } from \"./types.js\";\n\nfunction randomOctets(length: number): Uint8Array {\n const bytes = new Uint8Array(length);\n crypto.getRandomValues(bytes);\n return bytes;\n}\n\n/** Default Eristack `idFactory` — UUID v7, not DB serial or `randomUUID`. */\nexport function entityIdFactory(): EntityId {\n return generateEntityId();\n}\n\nexport function generateEntityId(unixMs: number = Date.now()): EntityId {\n const rnd = randomOctets(10);\n const bytes = generateEntityIdBytes(unixMs, rnd);\n return bytesToCanonicalUuid(bytes);\n}\n\n/** Deterministic generation for tests — same ms + random → same id. */\nexport function generateEntityIdAt(\n unixMs: number,\n random: Uint8Array,\n): EntityId {\n const bytes = generateEntityIdBytes(unixMs, random);\n return bytesToCanonicalUuid(bytes);\n}\n\nexport function normalizeEntityId(value: string): EntityId {\n const trimmed = value.trim().toLowerCase();\n const canonical = trimmed.includes(\"-\")\n ? trimmed\n : `${trimmed.slice(0, 8)}-${trimmed.slice(8, 12)}-${trimmed.slice(12, 16)}-${trimmed.slice(16, 20)}-${trimmed.slice(20)}`;\n\n if (!isHyphenatedUuidLower(canonical)) {\n throw new EntityIdParseError(\"Entity id must be a lowercase UUID string\");\n }\n\n const bytes = canonicalUuidToBytes(canonical);\n assertEntityIdV7(bytes);\n return canonical as EntityId;\n}\n\nexport function parseEntityId(value: string): EntityId {\n return normalizeEntityId(value);\n}\n\nexport function isValidEntityId(value: string): boolean {\n try {\n parseEntityId(value);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function entityIdToUnixMs(id: EntityId): number {\n const bytes = canonicalUuidToBytes(id);\n assertEntityIdV7(bytes);\n return unixMsFromV7Bytes(bytes);\n}\n\nexport function entityIdToDate(id: EntityId): Date {\n return new Date(entityIdToUnixMs(id));\n}\n\n/** Lexicographic compare — matches time order for ids from `generateEntityId`. */\nexport function compareEntityIds(a: EntityId, b: EntityId): number {\n return a.localeCompare(b);\n}\n\nexport function entityIdEquals(a: string, b: string): boolean {\n try {\n return parseEntityId(a) === parseEntityId(b);\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":[]}