@eristack/entity-id
Version:
UUID v7 entity identifiers — sortable, parseable, Drizzle column helper
1 lines • 10.7 kB
Source Map (JSON)
{"version":3,"sources":["../../src/drizzle/index.ts","../../src/drizzle/column.ts","../../src/core/errors.ts","../../src/core/types.ts","../../src/core/internal.ts","../../src/core/entity-id.ts"],"sourcesContent":["export type { DrizzleDialect, EntityIdColumnOptions } from \"./types.js\";\nexport { entityIdColumn, entityIdPgColumn } from \"./column.js\";\n","import { uuid } from \"drizzle-orm/pg-core\";\nimport { char as mysqlChar } from \"drizzle-orm/mysql-core\";\nimport { text } from \"drizzle-orm/sqlite-core\";\n\nimport { generateEntityId } from \"../core/entity-id.js\";\nimport type { DrizzleDialect, EntityIdColumnOptions } from \"./types.js\";\n\nfunction pgsqlEntityIdColumn(sqlName: string) {\n return uuid(sqlName).$defaultFn(() => generateEntityId());\n}\n\nfunction mysqlEntityIdColumn(sqlName: string) {\n return mysqlChar(sqlName, { length: 36 }).$defaultFn(() => generateEntityId());\n}\n\nfunction sqliteEntityIdColumn(sqlName: string) {\n return text(sqlName).$defaultFn(() => generateEntityId());\n}\n\nexport function entityIdColumn(\n dialect: \"pgsql\",\n name: string,\n options?: EntityIdColumnOptions,\n): ReturnType<typeof pgsqlEntityIdColumn>;\nexport function entityIdColumn(\n dialect: \"mysql\",\n name: string,\n options?: EntityIdColumnOptions,\n): ReturnType<typeof mysqlEntityIdColumn>;\nexport function entityIdColumn(\n dialect: \"sqlite\",\n name: string,\n options?: EntityIdColumnOptions,\n): ReturnType<typeof sqliteEntityIdColumn>;\nexport function entityIdColumn(\n dialect: DrizzleDialect,\n name: string,\n options?: EntityIdColumnOptions,\n): ReturnType<typeof pgsqlEntityIdColumn> | ReturnType<typeof mysqlEntityIdColumn> | ReturnType<typeof sqliteEntityIdColumn> {\n const sqlName = options?.sqlName ?? name;\n\n switch (dialect) {\n case \"pgsql\":\n return pgsqlEntityIdColumn(sqlName);\n case \"mysql\":\n return mysqlEntityIdColumn(sqlName);\n case \"sqlite\":\n return sqliteEntityIdColumn(sqlName);\n default: {\n const _exhaustive: never = dialect;\n throw new Error(`Unsupported dialect: ${String(_exhaustive)}`);\n }\n }\n}\n\n/** @deprecated Prefer `entityIdColumn` — pgsql-only alias kept for early docs drafts. */\nexport function entityIdPgColumn(name: string, options?: EntityIdColumnOptions) {\n return entityIdColumn(\"pgsql\", name, options);\n}\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;AAAA;;;ACAA,qBAAqB;AACrB,wBAAkC;AAClC,yBAAqB;;;ACFd,IAAM,uBAAuB;AAE7B,IAAM,qBAAN,cAAiC,MAAM;AAAA,EACnC,OAAO;AAAA,EAEhB,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;;;ACJO,IAAM,wBAAwB;;;ACE9B,SAAS,qBAAqB,OAA6B;AAChE,QAAM,MAAM,CAAC,GAAG,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC1E,QAAM,YACJ,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,EAAE,CAAC;AACnG,SAAO;AACT;AAsCO,SAAS,sBACd,QACA,QACY;AACZ,MAAI,CAAC,OAAO,SAAS,MAAM,KAAK,SAAS,KAAK,SAAS,uBAAuB;AAC5E,UAAM,IAAI;AAAA,MACR,kDAAkD,qBAAqB;AAAA,IACzE;AAAA,EACF;AACA,MAAI,OAAO,SAAS,IAAI;AACtB,UAAM,IAAI,mBAAmB,yCAAyC;AAAA,EACxE;AAEA,QAAM,KAAK,OAAO,KAAK,MAAM,MAAM,CAAC;AACpC,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,QAAM,CAAC,IAAI,OAAQ,MAAM,MAAO,KAAK;AACrC,QAAM,CAAC,IAAI,OAAQ,MAAM,MAAO,KAAK;AACrC,QAAM,CAAC,IAAI,OAAQ,MAAM,MAAO,KAAK;AACrC,QAAM,CAAC,IAAI,OAAQ,MAAM,MAAO,KAAK;AACrC,QAAM,CAAC,IAAI,OAAQ,MAAM,KAAM,KAAK;AACpC,QAAM,CAAC,IAAI,OAAO,KAAK,KAAK;AAE5B,QAAM,CAAC,IAAK,OAAO,CAAC,IAAK,KAAQ;AACjC,QAAM,CAAC,IAAI,OAAO,CAAC;AACnB,QAAM,CAAC,IAAK,OAAO,CAAC,IAAK,KAAQ;AACjC,QAAM,CAAC,IAAI,OAAO,CAAC;AACnB,QAAM,EAAE,IAAI,OAAO,CAAC;AACpB,QAAM,EAAE,IAAI,OAAO,CAAC;AACpB,QAAM,EAAE,IAAI,OAAO,CAAC;AACpB,QAAM,EAAE,IAAI,OAAO,CAAC;AACpB,QAAM,EAAE,IAAI,OAAO,CAAC;AACpB,QAAM,EAAE,IAAI,OAAO,CAAC;AAEpB,SAAO;AACT;;;ACzEA,SAAS,aAAa,QAA4B;AAChD,QAAM,QAAQ,IAAI,WAAW,MAAM;AACnC,SAAO,gBAAgB,KAAK;AAC5B,SAAO;AACT;AAOO,SAAS,iBAAiB,SAAiB,KAAK,IAAI,GAAa;AACtE,QAAM,MAAM,aAAa,EAAE;AAC3B,QAAM,QAAQ,sBAAsB,QAAQ,GAAG;AAC/C,SAAO,qBAAqB,KAAK;AACnC;;;AJnBA,SAAS,oBAAoB,SAAiB;AAC5C,aAAO,qBAAK,OAAO,EAAE,WAAW,MAAM,iBAAiB,CAAC;AAC1D;AAEA,SAAS,oBAAoB,SAAiB;AAC5C,aAAO,kBAAAA,MAAU,SAAS,EAAE,QAAQ,GAAG,CAAC,EAAE,WAAW,MAAM,iBAAiB,CAAC;AAC/E;AAEA,SAAS,qBAAqB,SAAiB;AAC7C,aAAO,yBAAK,OAAO,EAAE,WAAW,MAAM,iBAAiB,CAAC;AAC1D;AAiBO,SAAS,eACd,SACA,MACA,SAC2H;AAC3H,QAAM,UAAU,SAAS,WAAW;AAEpC,UAAQ,SAAS;AAAA,IACf,KAAK;AACH,aAAO,oBAAoB,OAAO;AAAA,IACpC,KAAK;AACH,aAAO,oBAAoB,OAAO;AAAA,IACpC,KAAK;AACH,aAAO,qBAAqB,OAAO;AAAA,IACrC,SAAS;AACP,YAAM,cAAqB;AAC3B,YAAM,IAAI,MAAM,wBAAwB,OAAO,WAAW,CAAC,EAAE;AAAA,IAC/D;AAAA,EACF;AACF;AAGO,SAAS,iBAAiB,MAAc,SAAiC;AAC9E,SAAO,eAAe,SAAS,MAAM,OAAO;AAC9C;","names":["mysqlChar"]}