UNPKG

@eristack/entity-id

Version:

UUID v7 entity identifiers — sortable, parseable, Drizzle column helper

1 lines • 8.35 kB
{"version":3,"sources":["../../src/zod/index.ts","../../src/zod/schemas.ts","../../src/core/errors.ts","../../src/core/types.ts","../../src/core/internal.ts","../../src/core/entity-id.ts"],"sourcesContent":["export { entityIdSchema } from \"./schemas.js\";\nexport type { EntityIdSchemaOutput } from \"./schemas.js\";\n","import { z } from \"zod\";\n\nimport { parseEntityId } from \"../core/entity-id.js\";\n\nexport const entityIdSchema = z\n .string()\n .transform((value, ctx) => {\n try {\n return parseEntityId(value);\n } catch (err) {\n ctx.addIssue({\n code: \"custom\",\n message: err instanceof Error ? err.message : \"Invalid entity id\",\n });\n return z.NEVER;\n }\n });\n\nexport type EntityIdSchemaOutput = z.infer<typeof entityIdSchema>;\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,iBAAkB;;;ACAX,IAAM,uBAAuB;AAE7B,IAAM,qBAAN,cAAiC,MAAM;AAAA,EACnC,OAAO;AAAA,EAEhB,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;;;ACNO,IAAM,oBAAoB;;;ACCjC,IAAM,aACJ;AASK,SAAS,qBAAqB,IAAwB;AAC3D,QAAM,UAAU,GAAG,QAAQ,MAAM,EAAE,EAAE,YAAY;AACjD,MAAI,QAAQ,WAAW,MAAM,CAAC,cAAc,KAAK,OAAO,GAAG;AACzD,UAAM,IAAI,mBAAmB,oDAAoD;AAAA,EACnF;AACA,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAM,CAAC,IAAI,OAAO,SAAS,QAAQ,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;AAAA,EAChE;AACA,SAAO;AACT;AAEO,SAAS,iBAAiB,OAAyB;AACxD,QAAM,UAAW,MAAM,CAAC,KAAM,IAAK;AACnC,MAAI,YAAY,mBAAmB;AACjC,UAAM,IAAI;AAAA,MACR,kCAAkC,iBAAiB,iBAAiB,OAAO;AAAA,IAC7E;AAAA,EACF;AACA,QAAM,UAAW,MAAM,CAAC,KAAM,IAAK;AACnC,MAAI,YAAY,GAAM;AACpB,UAAM,IAAI,mBAAmB,gDAAgD;AAAA,EAC/E;AACF;AAiDO,SAAS,sBAAsB,OAAwB;AAC5D,SAAO,WAAW,KAAK,KAAK;AAC9B;;;ACnDO,SAAS,kBAAkB,OAAyB;AACzD,QAAM,UAAU,MAAM,KAAK,EAAE,YAAY;AACzC,QAAM,YAAY,QAAQ,SAAS,GAAG,IAClC,UACA,GAAG,QAAQ,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ,MAAM,GAAG,EAAE,CAAC,IAAI,QAAQ,MAAM,IAAI,EAAE,CAAC,IAAI,QAAQ,MAAM,IAAI,EAAE,CAAC,IAAI,QAAQ,MAAM,EAAE,CAAC;AAEzH,MAAI,CAAC,sBAAsB,SAAS,GAAG;AACrC,UAAM,IAAI,mBAAmB,2CAA2C;AAAA,EAC1E;AAEA,QAAM,QAAQ,qBAAqB,SAAS;AAC5C,mBAAiB,KAAK;AACtB,SAAO;AACT;AAEO,SAAS,cAAc,OAAyB;AACrD,SAAO,kBAAkB,KAAK;AAChC;;;AJlDO,IAAM,iBAAiB,aAC3B,OAAO,EACP,UAAU,CAAC,OAAO,QAAQ;AACzB,MAAI;AACF,WAAO,cAAc,KAAK;AAAA,EAC5B,SAAS,KAAK;AACZ,QAAI,SAAS;AAAA,MACX,MAAM;AAAA,MACN,SAAS,eAAe,QAAQ,IAAI,UAAU;AAAA,IAChD,CAAC;AACD,WAAO,aAAE;AAAA,EACX;AACF,CAAC;","names":[]}