@smonn/ids 0.14.1 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +3 -3
  2. package/dist/{adapter-types-7wWdELSh.mjs → adapter-types-CjzFNDcJ.mjs} +7 -2
  3. package/dist/adapter-types-CjzFNDcJ.mjs.map +1 -0
  4. package/dist/cli.mjs +30 -22
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/drizzle.d.mts +87 -3
  7. package/dist/drizzle.d.mts.map +1 -1
  8. package/dist/drizzle.mjs +115 -5
  9. package/dist/drizzle.mjs.map +1 -1
  10. package/dist/express.d.mts +44 -2
  11. package/dist/express.d.mts.map +1 -1
  12. package/dist/express.mjs +60 -2
  13. package/dist/express.mjs.map +1 -1
  14. package/dist/fastify.d.mts +49 -2
  15. package/dist/fastify.d.mts.map +1 -1
  16. package/dist/fastify.mjs +61 -2
  17. package/dist/fastify.mjs.map +1 -1
  18. package/dist/graphql.d.mts +2 -1
  19. package/dist/graphql.d.mts.map +1 -1
  20. package/dist/graphql.mjs +2 -1
  21. package/dist/graphql.mjs.map +1 -1
  22. package/dist/hono.d.mts +44 -2
  23. package/dist/hono.d.mts.map +1 -1
  24. package/dist/hono.mjs +54 -2
  25. package/dist/hono.mjs.map +1 -1
  26. package/dist/kysely.d.mts +73 -2
  27. package/dist/kysely.d.mts.map +1 -1
  28. package/dist/kysely.mjs +84 -2
  29. package/dist/kysely.mjs.map +1 -1
  30. package/dist/mikro-orm.d.mts +42 -3
  31. package/dist/mikro-orm.d.mts.map +1 -1
  32. package/dist/mikro-orm.mjs +54 -4
  33. package/dist/mikro-orm.mjs.map +1 -1
  34. package/dist/nestjs.mjs +1 -1
  35. package/dist/{opaque-COAcIIY4.mjs → opaque-Dle3CmSE.mjs} +18 -10
  36. package/dist/opaque-Dle3CmSE.mjs.map +1 -0
  37. package/dist/opaque.d.mts +16 -10
  38. package/dist/opaque.d.mts.map +1 -1
  39. package/dist/opaque.mjs +1 -1
  40. package/dist/prisma.d.mts +112 -9
  41. package/dist/prisma.d.mts.map +1 -1
  42. package/dist/prisma.mjs +101 -3
  43. package/dist/prisma.mjs.map +1 -1
  44. package/dist/typeorm.d.mts +25 -1
  45. package/dist/typeorm.d.mts.map +1 -1
  46. package/dist/typeorm.mjs +35 -2
  47. package/dist/typeorm.mjs.map +1 -1
  48. package/package.json +1 -1
  49. package/dist/adapter-types-7wWdELSh.mjs.map +0 -1
  50. package/dist/opaque-COAcIIY4.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { a as toWireId, i as payloadBytesFromId, n as registerBrand, r as payloadBase32Length, s as validateBrand, t as wireMethods } from "./codec-shell-BRZkuQeP.mjs";
2
2
  import { a as writeTimestamp, r as readTimestampMs, t as defaultRng } from "./rng-6GyNT4zS.mjs";
3
- import { a as decryptPayload, i as encodeKeyMaterial, r as decodeKeyMaterial, s as encryptPayload, t as assertValidKeyMaterialByteLength } from "./key-material-1wOKJ1o-.mjs";
3
+ import { a as decryptPayload, i as encodeKeyMaterial, o as deriveKey, r as decodeKeyMaterial, s as encryptPayload, t as assertValidKeyMaterialByteLength } from "./key-material-1wOKJ1o-.mjs";
4
4
  //#region src/codecs/opaque/layout.ts
5
5
  function buildPlaintext(ms, rng) {
6
6
  const plaintext = /* @__PURE__ */ new Uint8Array(16);
@@ -31,21 +31,29 @@ function createOpaqueLayoutOps(prefix, key, rng) {
31
31
  }
32
32
  //#endregion
33
33
  //#region src/codecs/opaque/key.ts
34
+ const aesInfo = new TextEncoder().encode("@smonn/ids/opaque/aes");
34
35
  const opaqueKeyInternals = /* @__PURE__ */ new WeakMap();
35
36
  /**
36
- * Imports raw AES key bytes into an {@link OpaqueKey} handle for the Opaque
37
+ * Imports operator key material into an {@link OpaqueKey} handle for the Opaque
37
38
  * Timestamp codec.
38
39
  *
39
- * Accepts 16, 24, or 32 bytes (AES-128 / AES-192 / AES-256 strength).
40
- * To store or transport key material, use {@link encodeOpaqueKey} /
41
- * {@link decodeOpaqueKey} (`"hex"` or `"base64url"` not Crockford base32).
40
+ * The bytes are HKDF **input keying material**, not the AES key itself: the
41
+ * codec derives an **AES-256** key from them via HKDF under the label
42
+ * `@smonn/ids/opaque/aes` (ADR-0027). Accepts 16, 24, or 32 bytes; the input
43
+ * size sets the entropy floor only — a 16-byte handle still yields AES-256 with
44
+ * a 128-bit entropy floor. To store or transport key material, use
45
+ * {@link encodeOpaqueKey} / {@link decodeOpaqueKey} (`"hex"` or `"base64url"` —
46
+ * not Crockford base32).
42
47
  *
43
- * @param bytes - 16, 24, or 32 raw key bytes.
48
+ * @param bytes - 16, 24, or 32 bytes of raw key material.
44
49
  * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
45
50
  */
46
51
  async function importOpaqueKey(bytes) {
47
52
  assertValidKeyMaterialByteLength(bytes.length, "AES");
48
- const cryptoKey = await crypto.subtle.importKey("raw", bytes, "AES-CBC", false, ["encrypt", "decrypt"]);
53
+ const cryptoKey = await deriveKey(bytes, aesInfo, {
54
+ name: "AES-CBC",
55
+ length: 256
56
+ }, ["encrypt", "decrypt"]);
49
57
  const key = Object.freeze({});
50
58
  opaqueKeyInternals.set(key, cryptoKey);
51
59
  return key;
@@ -56,9 +64,9 @@ function getOpaqueKeyCryptoKey(key) {
56
64
  return cryptoKey;
57
65
  }
58
66
  /**
59
- * Encodes raw AES key bytes for storage in env vars or secret managers.
67
+ * Encodes raw Opaque key material bytes for storage in env vars or secret managers.
60
68
  *
61
- * @param bytes - 16, 24, or 32 raw key bytes (AES-128/192/256).
69
+ * @param bytes - 16, 24, or 32 raw Opaque key material bytes.
62
70
  * @param format - `hex` (lowercase) or `base64url`.
63
71
  * @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
64
72
  * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
@@ -113,4 +121,4 @@ function createOpaqueTimestampId(brand, opts) {
113
121
  //#endregion
114
122
  export { importOpaqueKey as i, decodeOpaqueKey as n, encodeOpaqueKey as r, createOpaqueTimestampId as t };
115
123
 
116
- //# sourceMappingURL=opaque-COAcIIY4.mjs.map
124
+ //# sourceMappingURL=opaque-Dle3CmSE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opaque-Dle3CmSE.mjs","names":[],"sources":["../src/codecs/opaque/layout.ts","../src/codecs/opaque/key.ts","../src/codecs/opaque/index.ts"],"sourcesContent":["import type { webcrypto } from \"node:crypto\";\nimport type { Id, LayoutOps, Prefix } from \"../../types.js\";\nimport { decryptPayload, encryptPayload } from \"../_kernel/crypto.js\";\nimport { payloadBytesFromId, toWireId } from \"../../wire/envelope.js\";\nimport { payloadBase32Length, payloadByteLength } from \"../../wire/invariants.js\";\nimport {\n readTimestampMs,\n timestampByteLength,\n writeTimestamp,\n} from \"../../wire/timestamp-bytes.js\";\n\nfunction buildPlaintext(ms: number, rng: (target: Uint8Array) => void): Uint8Array {\n const plaintext = new Uint8Array(payloadByteLength);\n writeTimestamp(ms, plaintext);\n rng(plaintext.subarray(timestampByteLength, payloadByteLength));\n return plaintext;\n}\n\nasync function extractTimestampFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n key: webcrypto.CryptoKey,\n id: Id<Brand>,\n): Promise<Date> {\n const plaintext = await decryptPayload(key, payloadBytesFromId(prefix, id));\n return new Date(readTimestampMs(plaintext));\n}\n\n/** Produces a canonical encrypted wire ID. Per-call plaintext/ciphertext buffers —\n * subtle dominates this path; reuse would be safe but not worth pinning to spec detail. */\nasync function generateWireId<Brand extends string>(\n prefix: Prefix<Brand>,\n key: webcrypto.CryptoKey,\n rng: (target: Uint8Array) => void,\n ms: number,\n): Promise<Id<Brand>> {\n const plaintext = buildPlaintext(ms, rng);\n const encrypted = await encryptPayload(key, plaintext);\n return toWireId(prefix, encrypted);\n}\n\n/** Structural placeholder for JSON Schema (encrypt is async). */\nfunction schemaExample<Brand extends string>(prefix: Prefix<Brand>): string {\n return prefix + \"0\".repeat(payloadBase32Length);\n}\n\n/** Layout ops binder for the Opaque Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */\nexport function createOpaqueLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n key: webcrypto.CryptoKey,\n rng: (target: Uint8Array) => void,\n): LayoutOps<Brand> & {\n generateAt(ms: number): Promise<Id<Brand>>;\n extractTimestamp(id: Id<Brand>): Promise<Date>;\n} {\n return {\n generateAt: (ms: number): Promise<Id<Brand>> => generateWireId(prefix, key, rng, ms),\n extractTimestamp: (id: Id<Brand>): Promise<Date> => extractTimestampFromId(prefix, key, id),\n exampleWireId: (_ms?: number): Id<Brand> => schemaExample(prefix) as Id<Brand>,\n };\n}\n","import type { webcrypto } from \"node:crypto\";\nimport { deriveKey } from \"../_kernel/crypto.js\";\nimport {\n assertValidKeyMaterialByteLength,\n decodeKeyMaterial,\n encodeKeyMaterial,\n} from \"../_kernel/key-material.js\";\n\n/** Wire encoding for opaque AES key material (not Crockford base32). */\nexport type OpaqueKeyFormat = \"hex\" | \"base64url\";\n\n// HKDF domain-separation label for the Opaque AES key; see ADR-0019 / ADR-0027.\nconst aesInfo = new TextEncoder().encode(\"@smonn/ids/opaque/aes\");\n\ndeclare const opaqueKeyBrand: unique symbol;\n\n/**\n * Opaque imported handle for the Opaque Timestamp codec's AES-256 key.\n *\n * Holds the underlying `webcrypto.CryptoKey` internally; callers never access it directly.\n * Obtain handles via {@link importOpaqueKey} and pass them to\n * `createOpaqueTimestampId` as the `key` option.\n *\n * The same raw secret may safely back an `OpaqueKey` and any other codec's\n * handle (a **primary secret**): each codec derives its key under a distinct\n * HKDF label, so the derived keys are independent — but each codec needs its\n * own explicit import. See ADR-0027.\n */\nexport type OpaqueKey = {\n readonly [opaqueKeyBrand]: \"OpaqueKey\";\n};\n\nconst opaqueKeyInternals = new WeakMap<OpaqueKey, webcrypto.CryptoKey>();\n\n/**\n * Imports operator key material into an {@link OpaqueKey} handle for the Opaque\n * Timestamp codec.\n *\n * The bytes are HKDF **input keying material**, not the AES key itself: the\n * codec derives an **AES-256** key from them via HKDF under the label\n * `@smonn/ids/opaque/aes` (ADR-0027). Accepts 16, 24, or 32 bytes; the input\n * size sets the entropy floor only — a 16-byte handle still yields AES-256 with\n * a 128-bit entropy floor. To store or transport key material, use\n * {@link encodeOpaqueKey} / {@link decodeOpaqueKey} (`\"hex\"` or `\"base64url\"` —\n * not Crockford base32).\n *\n * @param bytes - 16, 24, or 32 bytes of raw key material.\n * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.\n */\nexport async function importOpaqueKey(bytes: Uint8Array): Promise<OpaqueKey> {\n assertValidKeyMaterialByteLength(bytes.length, \"AES\");\n const cryptoKey = await deriveKey(bytes, aesInfo, { name: \"AES-CBC\", length: 256 }, [\n \"encrypt\",\n \"decrypt\",\n ]);\n const key = Object.freeze({}) as OpaqueKey;\n opaqueKeyInternals.set(key, cryptoKey);\n return key;\n}\n\nexport function getOpaqueKeyCryptoKey(key: OpaqueKey): webcrypto.CryptoKey {\n const cryptoKey = opaqueKeyInternals.get(key);\n if (cryptoKey === undefined) {\n throw new Error(\"invalid opaque key\");\n }\n return cryptoKey;\n}\n\n/**\n * Encodes raw Opaque key material bytes for storage in env vars or secret managers.\n *\n * @param bytes - 16, 24, or 32 raw Opaque key material bytes.\n * @param format - `hex` (lowercase) or `base64url`.\n * @throws {IdsError} `invalid_key_format` if `format` is not `\"hex\"` or `\"base64url\"`.\n * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.\n */\nexport function encodeOpaqueKey(bytes: Uint8Array, format: OpaqueKeyFormat): string {\n return encodeKeyMaterial(bytes, format, \"opaque\", \"AES\");\n}\n\n/**\n * Decodes key material emitted by `encodeOpaqueKey` (or `ids keygen`) back to raw bytes.\n *\n * @param encoded - Hex or base64url string.\n * @param format - Must match how the string was encoded.\n * @throws {IdsError} `invalid_key_format` if `format` is not `\"hex\"` or `\"base64url\"`.\n * @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.\n * @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.\n */\nexport function decodeOpaqueKey(encoded: string, format: OpaqueKeyFormat): Uint8Array {\n return decodeKeyMaterial(encoded, format, \"opaque\", \"AES\");\n}\n","import { validateBrand } from \"../_kernel/brand.js\";\nimport { createOpaqueLayoutOps } from \"./layout.js\";\nimport { getOpaqueKeyCryptoKey, type OpaqueKey } from \"./key.js\";\nimport { registerBrand } from \"../_kernel/registry.js\";\nimport { defaultRng } from \"../_kernel/rng.js\";\nimport type {\n Id,\n JsonSchema,\n ParseResult,\n Prefix,\n StandardSchemaProps,\n ValidBrand,\n} from \"../../types.js\";\nimport { wireMethods } from \"../../wire/codec-shell.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../../error.js\";\nexport {\n decodeOpaqueKey,\n encodeOpaqueKey,\n importOpaqueKey,\n type OpaqueKey,\n type OpaqueKeyFormat,\n} from \"./key.js\";\n\n/**\n * Configuration options for an Opaque Timestamp codec instance.\n */\nexport type OpaqueTimestampOptions = {\n /**\n * {@link OpaqueKey} handle for AES-CBC encryption and decryption.\n * Obtain via {@link importOpaqueKey}.\n *\n * A single key, not a ring: rotation is forward-only and caller-tracked —\n * hold one codec per key epoch and select it from your own records. The\n * library cannot trial keys (the payload is unauthenticated). See ADR-0013.\n */\n key: OpaqueKey;\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * A brand-scoped codec for generating and validating Opaque Timestamp IDs.\n *\n * Same wire shape as the Timestamp codec (`{brand}_` + 26 base32 chars) but the\n * payload is AES-CBC encrypted. `generate`, `generateAt`, and `extractTimestamp`\n * are async; parsing methods are sync. No `minIdForTime` / `maxIdForTime` —\n * encrypted payloads do not sort by creation time.\n *\n * @remarks\n * **Security properties (unauthenticated, deterministic, and malleable by design):**\n *\n * - The payload is AES-CBC encrypted but **unauthenticated** — there is no\n * integrity tag. A tampered or wrong-key payload decrypts to garbage bytes\n * without throwing.\n * - Opaque IDs must be treated as **opaque handles**, not as trusted or\n * authenticated tokens.\n * - `extractTimestamp` is best-effort on untrusted input: a wrong or tampered\n * key returns a plausible-looking `Date` without error, not a verification\n * failure. Do not treat the returned timestamp as proof of origin.\n */\nexport type OpaqueTimestampCodec<Brand extends string> = {\n /** Produces a new canonical encrypted ID using the codec's `now` and `rng`. */\n generate(): Promise<Id<Brand>>;\n /** Produces a new canonical encrypted ID with timestamp bytes from `date`. Throws on invalid dates. */\n generateAt(date: Date): Promise<Id<Brand>>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decrypts and decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n *\n * Requires the same key used at generation; a wrong key returns a plausible\n * but wrong `Date`, never an error. With rotation, select the codec for the\n * ID's key epoch from your own records — the library cannot. See ADR-0013.\n */\n extractTimestamp(id: Id<Brand>): Promise<Date>;\n /**\n * JSON Schema for the canonical wire form. The `pattern` matches the canonical stored\n * form only and is deliberately stricter than `parse()`/`safeParse()`, which accept\n * uppercase letters and Crockford aliases (`o`/`i`/`l`) before normalising. See ADR-0003.\n * The `example` is a structural placeholder (generated at construction time).\n */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n /**\n * Converts a trusted `Id<Brand>` to an RFC 9562 canonical (lowercase, hyphenated)\n * UUID string by reinterpreting the 16-byte payload verbatim. The payload is the\n * encrypted ciphertext — `toUUID` does not decrypt it. Total — cannot fail.\n * Returns a plain `string` (brand is shed). See ADR-0024.\n */\n toUUID(id: Id<Brand>): string;\n /**\n * Parses a UUID string into an `Id<Brand>`. Accepts case-insensitive `8-4-4-4-12`\n * hyphenated form only. Throws `IdsError` with `code: \"invalid_id\"` on bad input.\n * See ADR-0024.\n */\n fromUUID(value: string): Id<Brand>;\n /**\n * Non-throwing UUID parse. Returns `{ ok: true, id }` or\n * `{ ok: false, error: \"not_string\" | \"invalid_uuid\" }`. See ADR-0024.\n */\n safeFromUUID(value: unknown): ParseResult<Brand>;\n};\n\n/**\n * Creates an Opaque Timestamp codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Required `key` (an {@link OpaqueKey} from {@link importOpaqueKey}) plus\n * optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createOpaqueTimestampId<Brand extends string>(\n brand: Brand & ValidBrand<Brand>,\n opts: OpaqueTimestampOptions,\n): OpaqueTimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const cryptoKey = getOpaqueKeyCryptoKey(opts.key);\n const now = opts.now ?? Date.now;\n const rng = opts.rng ?? defaultRng;\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createOpaqueLayoutOps(prefix, cryptoKey, rng);\n\n return {\n generate: () => layout.generateAt(now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),\n \"~standard\": wire[\"~standard\"],\n toUUID: wire.toUUID,\n fromUUID: wire.fromUUID,\n safeFromUUID: wire.safeFromUUID,\n };\n}\n"],"mappings":";;;;AAWA,SAAS,eAAe,IAAY,KAA+C;CACjF,MAAM,4BAAY,IAAI,WAAA,EAA4B;CAClD,eAAe,IAAI,SAAS;CAC5B,IAAI,UAAU,SAAA,GAAA,EAA+C,CAAC;CAC9D,OAAO;AACT;AAEA,eAAe,uBACb,QACA,KACA,IACe;CACf,MAAM,YAAY,MAAM,eAAe,KAAK,mBAAmB,QAAQ,EAAE,CAAC;CAC1E,OAAO,IAAI,KAAK,gBAAgB,SAAS,CAAC;AAC5C;;;AAIA,eAAe,eACb,QACA,KACA,KACA,IACoB;CAGpB,OAAO,SAAS,QAAQ,MADA,eAAe,KADrB,eAAe,IAAI,GACe,CAAC,CACpB;AACnC;;AAGA,SAAS,cAAoC,QAA+B;CAC1E,OAAO,SAAS,IAAI,OAAO,mBAAmB;AAChD;;AAGA,SAAgB,sBACd,QACA,KACA,KAIA;CACA,OAAO;EACL,aAAa,OAAmC,eAAe,QAAQ,KAAK,KAAK,EAAE;EACnF,mBAAmB,OAAiC,uBAAuB,QAAQ,KAAK,EAAE;EAC1F,gBAAgB,QAA4B,cAAc,MAAM;CAClE;AACF;;;AC/CA,MAAM,UAAU,IAAI,YAAY,CAAC,CAAC,OAAO,uBAAuB;AAoBhE,MAAM,qCAAqB,IAAI,QAAwC;;;;;;;;;;;;;;;;AAiBvE,eAAsB,gBAAgB,OAAuC;CAC3E,iCAAiC,MAAM,QAAQ,KAAK;CACpD,MAAM,YAAY,MAAM,UAAU,OAAO,SAAS;EAAE,MAAM;EAAW,QAAQ;CAAI,GAAG,CAClF,WACA,SACF,CAAC;CACD,MAAM,MAAM,OAAO,OAAO,CAAC,CAAC;CAC5B,mBAAmB,IAAI,KAAK,SAAS;CACrC,OAAO;AACT;AAEA,SAAgB,sBAAsB,KAAqC;CACzE,MAAM,YAAY,mBAAmB,IAAI,GAAG;CAC5C,IAAI,cAAc,KAAA,GAChB,MAAM,IAAI,MAAM,oBAAoB;CAEtC,OAAO;AACT;;;;;;;;;AAUA,SAAgB,gBAAgB,OAAmB,QAAiC;CAClF,OAAO,kBAAkB,OAAO,QAAQ,UAAU,KAAK;AACzD;;;;;;;;;;AAWA,SAAgB,gBAAgB,SAAiB,QAAqC;CACpF,OAAO,kBAAkB,SAAS,QAAQ,UAAU,KAAK;AAC3D;;;;;;;;;;ACqCA,SAAgB,wBACd,OACA,MAC6B;CAC7B,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAE7C,MAAM,YAAY,sBAAsB,KAAK,GAAG;CAChD,MAAM,MAAM,KAAK,OAAO,KAAK;CAC7B,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,SAAwB,GAAG,MAAM;CACvC,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,SAAS,sBAAsB,QAAQ,WAAW,GAAG;CAE3D,OAAO;EACL,gBAAgB,OAAO,WAAW,IAAI,CAAC;EACvC,aAAa,SAAe,OAAO,WAAW,KAAK,QAAQ,CAAC;EAC5D,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,WAAW,KAAK;EAChB,kBAAkB,OAAO;EACzB,oBAAoB,KAAK,aAAa,OAAO,OAAO,cAAc,CAAC;EACnE,aAAa,KAAK;EAClB,QAAQ,KAAK;EACb,UAAU,KAAK;EACf,cAAc,KAAK;CACrB;AACF"}
package/dist/opaque.d.mts CHANGED
@@ -6,34 +6,40 @@ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-CifcK
6
6
  type OpaqueKeyFormat = "hex" | "base64url";
7
7
  declare const opaqueKeyBrand: unique symbol;
8
8
  /**
9
- * Opaque imported handle for one AES key used by the Opaque Timestamp codec.
9
+ * Opaque imported handle for the Opaque Timestamp codec's AES-256 key.
10
10
  *
11
11
  * Holds the underlying `webcrypto.CryptoKey` internally; callers never access it directly.
12
12
  * Obtain handles via {@link importOpaqueKey} and pass them to
13
13
  * `createOpaqueTimestampId` as the `key` option.
14
14
  *
15
- * Distinct from the `WrappingKey` used by `@smonn/ids/wrapped` one raw
16
- * secret must not silently serve both codecs without an explicit import.
15
+ * The same raw secret may safely back an `OpaqueKey` and any other codec's
16
+ * handle (a **primary secret**): each codec derives its key under a distinct
17
+ * HKDF label, so the derived keys are independent — but each codec needs its
18
+ * own explicit import. See ADR-0027.
17
19
  */
18
20
  type OpaqueKey = {
19
21
  readonly [opaqueKeyBrand]: "OpaqueKey";
20
22
  };
21
23
  /**
22
- * Imports raw AES key bytes into an {@link OpaqueKey} handle for the Opaque
24
+ * Imports operator key material into an {@link OpaqueKey} handle for the Opaque
23
25
  * Timestamp codec.
24
26
  *
25
- * Accepts 16, 24, or 32 bytes (AES-128 / AES-192 / AES-256 strength).
26
- * To store or transport key material, use {@link encodeOpaqueKey} /
27
- * {@link decodeOpaqueKey} (`"hex"` or `"base64url"` not Crockford base32).
27
+ * The bytes are HKDF **input keying material**, not the AES key itself: the
28
+ * codec derives an **AES-256** key from them via HKDF under the label
29
+ * `@smonn/ids/opaque/aes` (ADR-0027). Accepts 16, 24, or 32 bytes; the input
30
+ * size sets the entropy floor only — a 16-byte handle still yields AES-256 with
31
+ * a 128-bit entropy floor. To store or transport key material, use
32
+ * {@link encodeOpaqueKey} / {@link decodeOpaqueKey} (`"hex"` or `"base64url"` —
33
+ * not Crockford base32).
28
34
  *
29
- * @param bytes - 16, 24, or 32 raw key bytes.
35
+ * @param bytes - 16, 24, or 32 bytes of raw key material.
30
36
  * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
31
37
  */
32
38
  declare function importOpaqueKey(bytes: Uint8Array): Promise<OpaqueKey>;
33
39
  /**
34
- * Encodes raw AES key bytes for storage in env vars or secret managers.
40
+ * Encodes raw Opaque key material bytes for storage in env vars or secret managers.
35
41
  *
36
- * @param bytes - 16, 24, or 32 raw key bytes (AES-128/192/256).
42
+ * @param bytes - 16, 24, or 32 raw Opaque key material bytes.
37
43
  * @param format - `hex` (lowercase) or `base64url`.
38
44
  * @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
39
45
  * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
@@ -1 +1 @@
1
- {"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/codecs/opaque/key.ts","../src/codecs/opaque/index.ts"],"mappings":";;;;;KAQY,eAAA;AAAA,cAEE,cAAA;AAFd;;;;AAAY;AAA0B;;;;AAExB;AAFd,KAcY,SAAA;EAAA,UACA,cAAA;AAAA;;AAAA;AAgBZ;;;;;;;;;iBAAsB,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;;;AAAA;AA8BlE;;;;;iBAAgB,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;AAAA;AAa3D;;;;;iBAAgB,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;;;KCtD/D,sBAAA;EDpB0B;;;;AAExB;AAYd;;;ECeE,GAAA,EAAK,SAAA,EDdK;ECgBV,GAAA,iBDAoB;ECEpB,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;;;;;;;;ADJgE;AA8BlE;;;;;;;;;AAA2D;AAa3D;;KChBY,oBAAA;EDgB+D,+ECdzE,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA;EAEvB,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;;;ADYsC;;ECPzE,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;AA/ClC;;EAmDE,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EAtCX;;;EA0Cf,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;;;AAxCvC;AAuBF;EAyBE,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA,CAAQ,IAAA;;;;;;;EAOzC,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;;;;;;;EAO1C,MAAA,CAAO,EAAA,EAAI,EAAA,CAAG,KAAA;;;;;;EAMd,QAAA,CAAS,KAAA,WAAgB,EAAA,CAAG,KAAA;;;;;EAK5B,YAAA,CAAa,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;;;;iBAU5B,uBAAA,uBACd,KAAA,EAAO,KAAA,GAAQ,UAAA,CAAW,KAAA,GAC1B,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
1
+ {"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/codecs/opaque/key.ts","../src/codecs/opaque/index.ts"],"mappings":";;;;;KASY,eAAA;AAAA,cAKE,cAAA;AALd;;;;AAAY;AAA0B;;;;AAKxB;AAcd;;AAnBA,KAmBY,SAAA;EAAA,UACA,cAAA;AAAA;AAoBZ;;;;;;;;;;;;;AAAkE;AA2BlE;AA3BA,iBAAsB,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;;;;;;;AA2BP;AAa3D;iBAbgB,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;;;;;AAagB;;iBAA3D,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;;;KC7D/D,sBAAA;EDnB0B;;;;AAKxB;AAcd;;;ECSE,GAAA,EAAK,SAAA,EDRK;ECUV,GAAA,iBDUoB;ECRpB,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;;;;;;;;ADMgE;AA2BlE;;;;;;;;;AAA2D;AAa3D;;KCvBY,oBAAA;EDuB+D,+ECrBzE,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA;EAEvB,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;;;ADmBsC;;ECdzE,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;AA/ClC;;EAmDE,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EAtCX;;;EA0Cf,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;;;AAxCvC;AAuBF;EAyBE,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA,CAAQ,IAAA;;;;;;;EAOzC,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;;;;;;;EAO1C,MAAA,CAAO,EAAA,EAAI,EAAA,CAAG,KAAA;;;;;;EAMd,QAAA,CAAS,KAAA,WAAgB,EAAA,CAAG,KAAA;;;;;EAK5B,YAAA,CAAa,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;;;;iBAU5B,uBAAA,uBACd,KAAA,EAAO,KAAA,GAAQ,UAAA,CAAW,KAAA,GAC1B,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
package/dist/opaque.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-COAcIIY4.mjs";
2
+ import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-Dle3CmSE.mjs";
3
3
  export { IdsError, createOpaqueTimestampId, decodeOpaqueKey, encodeOpaqueKey, importOpaqueKey, isIdsError };
package/dist/prisma.d.mts CHANGED
@@ -1,9 +1,48 @@
1
1
  import { t as Id } from "./types-hGBnCpJj.mjs";
2
2
  import { n as IdColumnCodec } from "./adapter-types-Bia_w9sg.mjs";
3
3
  import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-CifcKKOG.mjs";
4
+ import { ModelQueryOptionsCb } from "@prisma/client/runtime/library";
4
5
 
5
6
  //#region src/adapters/prisma.d.ts
6
7
  /**
8
+ * Extension of {@link IdColumnCodec} that also exposes synchronous `generate()`.
9
+ * Required by {@link idField} so that {@link IdTransform.defaultQuery} can produce
10
+ * IDs at write time. Every full codec variant (Timestamp, Reverse Timestamp) satisfies
11
+ * this; async-generate codecs (Opaque, Signed, Wrapped, Digest) do not and are
12
+ * therefore unsupported by `defaultQuery`.
13
+ */
14
+ type IdGeneratingCodec<Brand extends string> = IdColumnCodec<Brand> & {
15
+ generate(): Id<Brand>;
16
+ };
17
+ /**
18
+ * The per-model object returned by {@link IdTransform.defaultQuery}, suitable for
19
+ * the model-level value inside a Prisma `$extends({ query: { modelName: … } })` block.
20
+ * Structurally identical to `{ [operation: string]: ModelQueryOptionsCb }` from
21
+ * `@prisma/client/runtime/library`.
22
+ */
23
+ type IdQueryField = {
24
+ [operation: string]: ModelQueryOptionsCb;
25
+ };
26
+ /**
27
+ * Typed `$extends` result-component field definition produced by
28
+ * {@link IdTransform.computeField} — a `{ needs, compute }` pair whose `compute`
29
+ * return type is statically `Id<Brand>`, so the extended-client model field is
30
+ * typed correctly without a per-call-site cast.
31
+ */
32
+ type IdComputeField<Brand extends string> = {
33
+ needs: Record<string, boolean>;
34
+ compute: (model: Record<string, unknown>) => Id<Brand>;
35
+ };
36
+ /**
37
+ * Typed `$extends` result-component field definition produced by
38
+ * {@link IdTransform.computeNullableField} — like {@link IdComputeField} but
39
+ * `compute` returns `Id<Brand> | null` for nullable columns.
40
+ */
41
+ type NullableIdComputeField<Brand extends string> = {
42
+ needs: Record<string, boolean>;
43
+ compute: (model: Record<string, unknown>) => Id<Brand> | null;
44
+ };
45
+ /**
7
46
  * Read/write transform pair and `$extends` result-component factory for
8
47
  * integrating `Id<Brand>` with Prisma extensions.
9
48
  */
@@ -15,6 +54,11 @@ type IdTransform<Brand extends string> = {
15
54
  */
16
55
  read(value: unknown): Id<Brand>;
17
56
  /**
57
+ * Nullable read transform: returns `null` when `value` is `null` or `undefined`;
58
+ * otherwise delegates to {@link read}. Use for optional foreign keys.
59
+ */
60
+ readNullable(value: unknown): Id<Brand> | null;
61
+ /**
18
62
  * Write transform: passes `Id<Brand>` through as its canonical string form.
19
63
  * `Id<Brand>` is already the canonical string, so this is an identity function
20
64
  * at runtime.
@@ -27,8 +71,8 @@ type IdTransform<Brand extends string> = {
27
71
  * `Id<Brand>` through Prisma's type machinery without a per-call-site cast.
28
72
  *
29
73
  * @param fieldName - The model field to read from (e.g. `"id"`).
30
- * @returns A `{ needs, compute }` object whose `compute` return type is
31
- * statically `Id<Brand>`, so the extended-client model field is typed correctly.
74
+ * @returns An {@link IdComputeField} whose `compute` return type is statically
75
+ * `Id<Brand>`, so the extended-client model field is typed correctly.
32
76
  *
33
77
  * @example
34
78
  * ```ts
@@ -40,20 +84,51 @@ type IdTransform<Brand extends string> = {
40
84
  * // xprisma.user.findUnique(…).id is typed as Id<"usr"> — no cast required
41
85
  * ```
42
86
  */
43
- computeField(fieldName: string): {
44
- needs: Record<string, boolean>;
45
- compute: (model: Record<string, unknown>) => Id<Brand>;
46
- };
87
+ computeField(fieldName: string): IdComputeField<Brand>;
88
+ /**
89
+ * Creates a `$extends` query-component model slice that auto-generates
90
+ * `Id<Brand>` values for `create`, `createMany`, and `upsert` operations
91
+ * when the field is absent, `undefined`, or `null` in `args.data` (or
92
+ * `args.create` for upsert). Explicitly supplied values are always passed
93
+ * through unchanged.
94
+ *
95
+ * @param fieldName - The model field to auto-generate (e.g. `"id"`).
96
+ * @returns An {@link IdQueryField} suitable for the model-level value inside
97
+ * a Prisma `$extends({ query: { modelName: … } })` block.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const xprisma = prisma.$extends({
102
+ * query: { user: userIdField.defaultQuery("id") },
103
+ * result: { user: { id: userIdField.computeField("id") } },
104
+ * });
105
+ * // id is auto-filled on create, and typed as Id<"usr"> on read
106
+ * await xprisma.user.create({ data: { name: "Alice" } });
107
+ * ```
108
+ */
109
+ defaultQuery(fieldName: string): IdQueryField;
110
+ /**
111
+ * Like {@link computeField} but for nullable columns — `compute` returns
112
+ * `Id<Brand> | null` instead of `Id<Brand>`.
113
+ *
114
+ * @param fieldName - The nullable model field to read from.
115
+ * @returns A {@link NullableIdComputeField} whose `compute` returns `Id<Brand> | null`.
116
+ */
117
+ computeNullableField(fieldName: string): NullableIdComputeField<Brand>;
47
118
  };
48
119
  /**
49
120
  * Creates a read/write transform pair for use with Prisma's `$extends` extension model.
50
121
  *
51
- * Works with any codec variant exposing `safeParse`.
122
+ * Requires a codec variant that exposes a synchronous `generate()` in addition to `safeParse` — see {@link IdGeneratingCodec}. Only the **Timestamp codec** and **Reverse Timestamp codec** qualify; Opaque, Signed, Wrapped, and Digest codecs cannot be passed to `idField()`.
52
123
  *
53
124
  * Use `computeField(fieldName)` to produce a typed `$extends` result-component
54
125
  * field definition — the brand is carried through Prisma's type machinery
55
126
  * automatically and no per-call-site cast is required.
56
127
  *
128
+ * For codecs that do not expose a synchronous `generate()` (Opaque Timestamp,
129
+ * Signed Timestamp, Wrapped key, Digest), use {@link idFieldReadOnly} instead —
130
+ * it accepts any {@link IdColumnCodec} and omits `defaultQuery`.
131
+ *
57
132
  * @example
58
133
  * ```ts
59
134
  * import { idField } from "@smonn/ids/prisma";
@@ -70,7 +145,35 @@ type IdTransform<Brand extends string> = {
70
145
  * // xprisma.user.findUnique(…).id is typed as Id<"usr"> — no cast required
71
146
  * ```
72
147
  */
73
- declare function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand>;
148
+ declare function idField<Brand extends string>(codec: IdGeneratingCodec<Brand>): IdTransform<Brand>;
149
+ /**
150
+ * Read-only sibling of {@link idField} for codec variants that do not expose a
151
+ * synchronous `generate()` — Opaque Timestamp, Signed Timestamp, Wrapped key,
152
+ * and Digest codecs all qualify.
153
+ *
154
+ * Accepts any {@link IdColumnCodec} (the wider constraint that only requires
155
+ * `safeParse`) and returns the full read/transform surface of {@link IdTransform}
156
+ * **minus `defaultQuery`**. Because `defaultQuery` is the only method that calls
157
+ * `generate()`, callers who only need the read path are not forced to provide a
158
+ * synchronous generator.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * import { idFieldReadOnly } from "@smonn/ids/prisma";
163
+ * import { createOpaqueTimestampId } from "@smonn/ids/opaque";
164
+ *
165
+ * const inv = createOpaqueTimestampId("inv", { key });
166
+ * const invoiceIdField = idFieldReadOnly(inv);
167
+ *
168
+ * const xprisma = prisma.$extends({
169
+ * result: {
170
+ * invoice: { id: invoiceIdField.computeField("id") },
171
+ * },
172
+ * });
173
+ * // xprisma.invoice.findUnique(…).id is typed as Id<"inv"> — no cast required
174
+ * ```
175
+ */
176
+ declare function idFieldReadOnly<Brand extends string>(codec: IdColumnCodec<Brand>): Omit<IdTransform<Brand>, "defaultQuery">;
74
177
  //#endregion
75
- export { type IdColumnCodec, IdTransform, IdsError, type IdsErrorCode, idField, isIdsError };
178
+ export { type IdColumnCodec, IdComputeField, IdGeneratingCodec, IdQueryField, IdTransform, IdsError, type IdsErrorCode, NullableIdComputeField, idField, idFieldReadOnly, isIdsError };
76
179
  //# sourceMappingURL=prisma.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/adapters/prisma.ts"],"mappings":";;;;;AAYA;;;;AAAA,KAAY,WAAA;;;;;;EAMV,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;EA6BsB;;;;;;;EArB/C,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;;;;;;;;;;;;;;AAqBkC;AA6BpD;;;;EA/BE,YAAA,CAAa,SAAA;IACX,KAAA,EAAO,MAAA;IACP,OAAA,GAAU,KAAA,EAAO,MAAA,sBAA4B,EAAA,CAAG,KAAA;EAAA;AAAA;;;;;;;;AA6BoC;;;;;;;;;;;;;;;;;;iBAAxE,OAAA,uBAA8B,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,WAAA,CAAY,KAAA"}
1
+ {"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/adapters/prisma.ts"],"mappings":";;;;;;AAgBA;;;;;;;AAAA,KAAY,iBAAA,yBAA0C,aAAA,CAAc,KAAA;EAClE,QAAA,IAAY,EAAA,CAAG,KAAA;AAAA;;;;;;;KASL,YAAA;EAAA,CAAsC,SAAA,WAAA,mBAAA;AAAA;;;AAAA;AAQlD;;;KAAY,cAAA;EACV,KAAA,EAAO,MAAA;EACP,OAAA,GAAU,KAAA,EAAO,MAAA,sBAA4B,EAAA,CAAG,KAAA;AAAA;;;;;;KAQtC,sBAAA;EACV,KAAA,EAAO,MAAA;EACP,OAAA,GAAU,KAAA,EAAO,MAAA,sBAA4B,EAAA,CAAG,KAAA;AAAA;;;AAVA;AAQlD;KASY,WAAA;;;;;;EAMV,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;EAboB;;;;EAkB7C,YAAA,CAAa,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;;;AAlBe;AAOlD;EAmBE,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;;;;;;;;;;;;;;;;;;;EAmBhB,YAAA,CAAa,SAAA,WAAoB,cAAA,CAAe,KAAA;;;;;;;;;;;;;;;;;;;;;AA8BgB;EARhE,YAAA,CAAa,SAAA,WAAoB,YAAA;EAwCnB;;;;;;;EAhCd,oBAAA,CAAqB,SAAA,WAAoB,sBAAA,CAAuB,KAAA;AAAA;;;;;;;AAgC0B;AA+F5F;;;;;;;;;;;;;;;;;;AAEoB;;;;iBAjGJ,OAAA,uBAA8B,KAAA,EAAO,iBAAA,CAAkB,KAAA,IAAS,WAAA,CAAY,KAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+F5E,eAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,IAAA,CAAK,WAAA,CAAY,KAAA"}
package/dist/prisma.mjs CHANGED
@@ -1,15 +1,19 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
2
+ import { n as readIdColumnNullable, t as readIdColumn } from "./adapter-types-CjzFNDcJ.mjs";
3
3
  //#region src/adapters/prisma.ts
4
4
  /**
5
5
  * Creates a read/write transform pair for use with Prisma's `$extends` extension model.
6
6
  *
7
- * Works with any codec variant exposing `safeParse`.
7
+ * Requires a codec variant that exposes a synchronous `generate()` in addition to `safeParse` — see {@link IdGeneratingCodec}. Only the **Timestamp codec** and **Reverse Timestamp codec** qualify; Opaque, Signed, Wrapped, and Digest codecs cannot be passed to `idField()`.
8
8
  *
9
9
  * Use `computeField(fieldName)` to produce a typed `$extends` result-component
10
10
  * field definition — the brand is carried through Prisma's type machinery
11
11
  * automatically and no per-call-site cast is required.
12
12
  *
13
+ * For codecs that do not expose a synchronous `generate()` (Opaque Timestamp,
14
+ * Signed Timestamp, Wrapped key, Digest), use {@link idFieldReadOnly} instead —
15
+ * it accepts any {@link IdColumnCodec} and omits `defaultQuery`.
16
+ *
13
17
  * @example
14
18
  * ```ts
15
19
  * import { idField } from "@smonn/ids/prisma";
@@ -27,10 +31,98 @@ import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
27
31
  * ```
28
32
  */
29
33
  function idField(codec) {
34
+ const { generate } = codec;
35
+ return {
36
+ read(value) {
37
+ return readIdColumn(codec, value);
38
+ },
39
+ readNullable(value) {
40
+ return readIdColumnNullable(codec, value);
41
+ },
42
+ write(value) {
43
+ return value;
44
+ },
45
+ computeField(fieldName) {
46
+ return {
47
+ needs: { [fieldName]: true },
48
+ compute: (model) => readIdColumn(codec, model[fieldName])
49
+ };
50
+ },
51
+ defaultQuery(fieldName) {
52
+ function injectIfAbsent(data) {
53
+ if (data[fieldName] == null) return {
54
+ ...data,
55
+ [fieldName]: generate()
56
+ };
57
+ return data;
58
+ }
59
+ return {
60
+ async create({ args, query }) {
61
+ const data = args.data;
62
+ return query(data != null ? {
63
+ ...args,
64
+ data: injectIfAbsent(data)
65
+ } : args);
66
+ },
67
+ async createMany({ args, query }) {
68
+ const data = args.data;
69
+ return query(Array.isArray(data) ? {
70
+ ...args,
71
+ data: data.map((item) => injectIfAbsent(item))
72
+ } : args);
73
+ },
74
+ async upsert({ args, query }) {
75
+ const createData = args.create;
76
+ return query(createData != null ? {
77
+ ...args,
78
+ create: injectIfAbsent(createData)
79
+ } : args);
80
+ }
81
+ };
82
+ },
83
+ computeNullableField(fieldName) {
84
+ return {
85
+ needs: { [fieldName]: true },
86
+ compute: (model) => readIdColumnNullable(codec, model[fieldName])
87
+ };
88
+ }
89
+ };
90
+ }
91
+ /**
92
+ * Read-only sibling of {@link idField} for codec variants that do not expose a
93
+ * synchronous `generate()` — Opaque Timestamp, Signed Timestamp, Wrapped key,
94
+ * and Digest codecs all qualify.
95
+ *
96
+ * Accepts any {@link IdColumnCodec} (the wider constraint that only requires
97
+ * `safeParse`) and returns the full read/transform surface of {@link IdTransform}
98
+ * **minus `defaultQuery`**. Because `defaultQuery` is the only method that calls
99
+ * `generate()`, callers who only need the read path are not forced to provide a
100
+ * synchronous generator.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * import { idFieldReadOnly } from "@smonn/ids/prisma";
105
+ * import { createOpaqueTimestampId } from "@smonn/ids/opaque";
106
+ *
107
+ * const inv = createOpaqueTimestampId("inv", { key });
108
+ * const invoiceIdField = idFieldReadOnly(inv);
109
+ *
110
+ * const xprisma = prisma.$extends({
111
+ * result: {
112
+ * invoice: { id: invoiceIdField.computeField("id") },
113
+ * },
114
+ * });
115
+ * // xprisma.invoice.findUnique(…).id is typed as Id<"inv"> — no cast required
116
+ * ```
117
+ */
118
+ function idFieldReadOnly(codec) {
30
119
  return {
31
120
  read(value) {
32
121
  return readIdColumn(codec, value);
33
122
  },
123
+ readNullable(value) {
124
+ return readIdColumnNullable(codec, value);
125
+ },
34
126
  write(value) {
35
127
  return value;
36
128
  },
@@ -39,10 +131,16 @@ function idField(codec) {
39
131
  needs: { [fieldName]: true },
40
132
  compute: (model) => readIdColumn(codec, model[fieldName])
41
133
  };
134
+ },
135
+ computeNullableField(fieldName) {
136
+ return {
137
+ needs: { [fieldName]: true },
138
+ compute: (model) => readIdColumnNullable(codec, model[fieldName])
139
+ };
42
140
  }
43
141
  };
44
142
  }
45
143
  //#endregion
46
- export { IdsError, idField, isIdsError };
144
+ export { IdsError, idField, idFieldReadOnly, isIdsError };
47
145
 
48
146
  //# sourceMappingURL=prisma.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.mjs","names":[],"sources":["../src/adapters/prisma.ts"],"sourcesContent":["import { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * Read/write transform pair and `$extends` result-component factory for\n * integrating `Id<Brand>` with Prisma extensions.\n */\nexport type IdTransform<Brand extends string> = {\n /**\n * Read transform: validates the raw database value via `safeParse` and returns\n * `Id<Brand>`. Throws if the value is missing, malformed, or belongs to a\n * different brand.\n */\n read(value: unknown): Id<Brand>;\n /**\n * Write transform: passes `Id<Brand>` through as its canonical string form.\n * `Id<Brand>` is already the canonical string, so this is an identity function\n * at runtime.\n *\n * Use in a Prisma `$extends` query component or explicit `data` mapping.\n */\n write(value: Id<Brand>): string;\n /**\n * Creates a typed `$extends` result-component field definition that carries\n * `Id<Brand>` through Prisma's type machinery without a per-call-site cast.\n *\n * @param fieldName - The model field to read from (e.g. `\"id\"`).\n * @returns A `{ needs, compute }` object whose `compute` return type is\n * statically `Id<Brand>`, so the extended-client model field is typed correctly.\n *\n * @example\n * ```ts\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\n computeField(fieldName: string): {\n needs: Record<string, boolean>;\n compute: (model: Record<string, unknown>) => Id<Brand>;\n };\n};\n\n/**\n * Creates a read/write transform pair for use with Prisma's `$extends` extension model.\n *\n * Works with any codec variant exposing `safeParse`.\n *\n * Use `computeField(fieldName)` to produce a typed `$extends` result-component\n * field definition — the brand is carried through Prisma's type machinery\n * automatically and no per-call-site cast is required.\n *\n * @example\n * ```ts\n * import { idField } from \"@smonn/ids/prisma\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const userIdField = idField(usr);\n *\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\nexport function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand> {\n return {\n read(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n computeField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n // Prisma's $extends types `compute` as returning `any` in its constraint\n // type (DynamicResultExtensionArgs). Returning a pre-built object with an\n // explicit Id<Brand> return type on `compute` causes TypeScript to infer\n // the brand through the `& R` intersection in $extends — encapsulating\n // the single necessary cast here rather than pushing it to every call site.\n compute: (model: Record<string, unknown>): Id<Brand> =>\n readIdColumn(codec, model[fieldName]),\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,SAAgB,QAA8B,OAAiD;CAC7F,OAAO;EACL,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;EACA,aAAa,WAAmB;GAC9B,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAM3B,UAAU,UACR,aAAa,OAAO,MAAM,UAAU;GACxC;EACF;CACF;AACF"}
1
+ {"version":3,"file":"prisma.mjs","names":[],"sources":["../src/adapters/prisma.ts"],"sourcesContent":["import type { ModelQueryOptionsCb, ModelQueryOptionsCbArgs } from \"@prisma/client/runtime/library\";\nimport { readIdColumn, readIdColumnNullable, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * Extension of {@link IdColumnCodec} that also exposes synchronous `generate()`.\n * Required by {@link idField} so that {@link IdTransform.defaultQuery} can produce\n * IDs at write time. Every full codec variant (Timestamp, Reverse Timestamp) satisfies\n * this; async-generate codecs (Opaque, Signed, Wrapped, Digest) do not and are\n * therefore unsupported by `defaultQuery`.\n */\nexport type IdGeneratingCodec<Brand extends string> = IdColumnCodec<Brand> & {\n generate(): Id<Brand>;\n};\n\n/**\n * The per-model object returned by {@link IdTransform.defaultQuery}, suitable for\n * the model-level value inside a Prisma `$extends({ query: { modelName: … } })` block.\n * Structurally identical to `{ [operation: string]: ModelQueryOptionsCb }` from\n * `@prisma/client/runtime/library`.\n */\nexport type IdQueryField = { [operation: string]: ModelQueryOptionsCb };\n\n/**\n * Typed `$extends` result-component field definition produced by\n * {@link IdTransform.computeField} — a `{ needs, compute }` pair whose `compute`\n * return type is statically `Id<Brand>`, so the extended-client model field is\n * typed correctly without a per-call-site cast.\n */\nexport type IdComputeField<Brand extends string> = {\n needs: Record<string, boolean>;\n compute: (model: Record<string, unknown>) => Id<Brand>;\n};\n\n/**\n * Typed `$extends` result-component field definition produced by\n * {@link IdTransform.computeNullableField} — like {@link IdComputeField} but\n * `compute` returns `Id<Brand> | null` for nullable columns.\n */\nexport type NullableIdComputeField<Brand extends string> = {\n needs: Record<string, boolean>;\n compute: (model: Record<string, unknown>) => Id<Brand> | null;\n};\n\n/**\n * Read/write transform pair and `$extends` result-component factory for\n * integrating `Id<Brand>` with Prisma extensions.\n */\nexport type IdTransform<Brand extends string> = {\n /**\n * Read transform: validates the raw database value via `safeParse` and returns\n * `Id<Brand>`. Throws if the value is missing, malformed, or belongs to a\n * different brand.\n */\n read(value: unknown): Id<Brand>;\n /**\n * Nullable read transform: returns `null` when `value` is `null` or `undefined`;\n * otherwise delegates to {@link read}. Use for optional foreign keys.\n */\n readNullable(value: unknown): Id<Brand> | null;\n /**\n * Write transform: passes `Id<Brand>` through as its canonical string form.\n * `Id<Brand>` is already the canonical string, so this is an identity function\n * at runtime.\n *\n * Use in a Prisma `$extends` query component or explicit `data` mapping.\n */\n write(value: Id<Brand>): string;\n /**\n * Creates a typed `$extends` result-component field definition that carries\n * `Id<Brand>` through Prisma's type machinery without a per-call-site cast.\n *\n * @param fieldName - The model field to read from (e.g. `\"id\"`).\n * @returns An {@link IdComputeField} whose `compute` return type is statically\n * `Id<Brand>`, so the extended-client model field is typed correctly.\n *\n * @example\n * ```ts\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\n computeField(fieldName: string): IdComputeField<Brand>;\n /**\n * Creates a `$extends` query-component model slice that auto-generates\n * `Id<Brand>` values for `create`, `createMany`, and `upsert` operations\n * when the field is absent, `undefined`, or `null` in `args.data` (or\n * `args.create` for upsert). Explicitly supplied values are always passed\n * through unchanged.\n *\n * @param fieldName - The model field to auto-generate (e.g. `\"id\"`).\n * @returns An {@link IdQueryField} suitable for the model-level value inside\n * a Prisma `$extends({ query: { modelName: … } })` block.\n *\n * @example\n * ```ts\n * const xprisma = prisma.$extends({\n * query: { user: userIdField.defaultQuery(\"id\") },\n * result: { user: { id: userIdField.computeField(\"id\") } },\n * });\n * // id is auto-filled on create, and typed as Id<\"usr\"> on read\n * await xprisma.user.create({ data: { name: \"Alice\" } });\n * ```\n */\n defaultQuery(fieldName: string): IdQueryField;\n /**\n * Like {@link computeField} but for nullable columns — `compute` returns\n * `Id<Brand> | null` instead of `Id<Brand>`.\n *\n * @param fieldName - The nullable model field to read from.\n * @returns A {@link NullableIdComputeField} whose `compute` returns `Id<Brand> | null`.\n */\n computeNullableField(fieldName: string): NullableIdComputeField<Brand>;\n};\n\n/**\n * Creates a read/write transform pair for use with Prisma's `$extends` extension model.\n *\n * Requires a codec variant that exposes a synchronous `generate()` in addition to `safeParse` — see {@link IdGeneratingCodec}. Only the **Timestamp codec** and **Reverse Timestamp codec** qualify; Opaque, Signed, Wrapped, and Digest codecs cannot be passed to `idField()`.\n *\n * Use `computeField(fieldName)` to produce a typed `$extends` result-component\n * field definition — the brand is carried through Prisma's type machinery\n * automatically and no per-call-site cast is required.\n *\n * For codecs that do not expose a synchronous `generate()` (Opaque Timestamp,\n * Signed Timestamp, Wrapped key, Digest), use {@link idFieldReadOnly} instead —\n * it accepts any {@link IdColumnCodec} and omits `defaultQuery`.\n *\n * @example\n * ```ts\n * import { idField } from \"@smonn/ids/prisma\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const userIdField = idField(usr);\n *\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\nexport function idField<Brand extends string>(codec: IdGeneratingCodec<Brand>): IdTransform<Brand> {\n const { generate } = codec;\n return {\n read(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n readNullable(value: unknown): Id<Brand> | null {\n return readIdColumnNullable(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n computeField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n // Prisma's $extends types `compute` as returning `any` in its constraint\n // type (DynamicResultExtensionArgs). Returning a pre-built object with an\n // explicit Id<Brand> return type on `compute` causes TypeScript to infer\n // the brand through the `& R` intersection in $extends — encapsulating\n // the single necessary cast here rather than pushing it to every call site.\n compute: (model: Record<string, unknown>): Id<Brand> =>\n readIdColumn(codec, model[fieldName]),\n };\n },\n defaultQuery(fieldName: string): IdQueryField {\n function injectIfAbsent(data: Record<string, unknown>): Record<string, unknown> {\n if (data[fieldName] == null) {\n return { ...data, [fieldName]: generate() };\n }\n return data;\n }\n\n type QueryArg = Parameters<ModelQueryOptionsCbArgs[\"query\"]>[0];\n\n return {\n async create({ args, query }) {\n const data = args.data as Record<string, unknown> | null | undefined;\n const nextArgs =\n data != null ? ({ ...args, data: injectIfAbsent(data) } as unknown as QueryArg) : args;\n return query(nextArgs);\n },\n async createMany({ args, query }) {\n const data = args.data as Array<Record<string, unknown>> | null | undefined;\n const nextArgs = Array.isArray(data)\n ? ({ ...args, data: data.map((item) => injectIfAbsent(item)) } as unknown as QueryArg)\n : args;\n return query(nextArgs);\n },\n async upsert({ args, query }) {\n const createData = args.create as Record<string, unknown> | null | undefined;\n const nextArgs =\n createData != null\n ? ({ ...args, create: injectIfAbsent(createData) } as unknown as QueryArg)\n : args;\n return query(nextArgs);\n },\n };\n },\n computeNullableField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n compute: (model: Record<string, unknown>): Id<Brand> | null =>\n readIdColumnNullable(codec, model[fieldName]),\n };\n },\n };\n}\n\n/**\n * Read-only sibling of {@link idField} for codec variants that do not expose a\n * synchronous `generate()` — Opaque Timestamp, Signed Timestamp, Wrapped key,\n * and Digest codecs all qualify.\n *\n * Accepts any {@link IdColumnCodec} (the wider constraint that only requires\n * `safeParse`) and returns the full read/transform surface of {@link IdTransform}\n * **minus `defaultQuery`**. Because `defaultQuery` is the only method that calls\n * `generate()`, callers who only need the read path are not forced to provide a\n * synchronous generator.\n *\n * @example\n * ```ts\n * import { idFieldReadOnly } from \"@smonn/ids/prisma\";\n * import { createOpaqueTimestampId } from \"@smonn/ids/opaque\";\n *\n * const inv = createOpaqueTimestampId(\"inv\", { key });\n * const invoiceIdField = idFieldReadOnly(inv);\n *\n * const xprisma = prisma.$extends({\n * result: {\n * invoice: { id: invoiceIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.invoice.findUnique(…).id is typed as Id<\"inv\"> — no cast required\n * ```\n */\nexport function idFieldReadOnly<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): Omit<IdTransform<Brand>, \"defaultQuery\"> {\n return {\n read(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n readNullable(value: unknown): Id<Brand> | null {\n return readIdColumnNullable(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n computeField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n compute: (model: Record<string, unknown>): Id<Brand> =>\n readIdColumn(codec, model[fieldName]),\n };\n },\n computeNullableField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n compute: (model: Record<string, unknown>): Id<Brand> | null =>\n readIdColumnNullable(codec, model[fieldName]),\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyJA,SAAgB,QAA8B,OAAqD;CACjG,MAAM,EAAE,aAAa;CACrB,OAAO;EACL,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,aAAa,OAAkC;GAC7C,OAAO,qBAAqB,OAAO,KAAK;EAC1C;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;EACA,aAAa,WAAmB;GAC9B,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAM3B,UAAU,UACR,aAAa,OAAO,MAAM,UAAU;GACxC;EACF;EACA,aAAa,WAAiC;GAC5C,SAAS,eAAe,MAAwD;IAC9E,IAAI,KAAK,cAAc,MACrB,OAAO;KAAE,GAAG;MAAO,YAAY,SAAS;IAAE;IAE5C,OAAO;GACT;GAIA,OAAO;IACL,MAAM,OAAO,EAAE,MAAM,SAAS;KAC5B,MAAM,OAAO,KAAK;KAGlB,OAAO,MADL,QAAQ,OAAQ;MAAE,GAAG;MAAM,MAAM,eAAe,IAAI;KAAE,IAA4B,IAC/D;IACvB;IACA,MAAM,WAAW,EAAE,MAAM,SAAS;KAChC,MAAM,OAAO,KAAK;KAIlB,OAAO,MAHU,MAAM,QAAQ,IAAI,IAC9B;MAAE,GAAG;MAAM,MAAM,KAAK,KAAK,SAAS,eAAe,IAAI,CAAC;KAAE,IAC3D,IACiB;IACvB;IACA,MAAM,OAAO,EAAE,MAAM,SAAS;KAC5B,MAAM,aAAa,KAAK;KAKxB,OAAO,MAHL,cAAc,OACT;MAAE,GAAG;MAAM,QAAQ,eAAe,UAAU;KAAE,IAC/C,IACe;IACvB;GACF;EACF;EACA,qBAAqB,WAAmB;GACtC,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAC3B,UAAU,UACR,qBAAqB,OAAO,MAAM,UAAU;GAChD;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,gBACd,OAC0C;CAC1C,OAAO;EACL,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,aAAa,OAAkC;GAC7C,OAAO,qBAAqB,OAAO,KAAK;EAC1C;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;EACA,aAAa,WAAmB;GAC9B,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAC3B,UAAU,UACR,aAAa,OAAO,MAAM,UAAU;GACxC;EACF;EACA,qBAAqB,WAAmB;GACtC,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAC3B,UAAU,UACR,qBAAqB,OAAO,MAAM,UAAU;GAChD;EACF;CACF;AACF"}
@@ -36,6 +36,30 @@ import { ValueTransformer } from "typeorm";
36
36
  * ```
37
37
  */
38
38
  declare function idTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer;
39
+ /**
40
+ * TypeORM column transformer for a **nullable** `Id<Brand>` column.
41
+ *
42
+ * Behaves like {@link idTransformer} but `from` returns `null` for `null` /
43
+ * `undefined` database values and `to` passes `null` / `undefined` through
44
+ * unchanged. Use for optional foreign keys.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { nullableIdTransformer } from "@smonn/ids/typeorm";
49
+ * import { createTimestampId } from "@smonn/ids";
50
+ * import type { Id } from "@smonn/ids";
51
+ * import { Column, Entity } from "typeorm";
52
+ *
53
+ * const usr = createTimestampId("usr");
54
+ *
55
+ * @Entity()
56
+ * class Post {
57
+ * @Column({ type: "text", nullable: true, transformer: nullableIdTransformer(usr) })
58
+ * authorId!: Id<"usr"> | null;
59
+ * }
60
+ * ```
61
+ */
62
+ declare function nullableIdTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer;
39
63
  //#endregion
40
- export { type IdColumnCodec, IdsError, type IdsErrorCode, idTransformer, isIdsError };
64
+ export { type IdColumnCodec, IdsError, type IdsErrorCode, idTransformer, isIdsError, nullableIdTransformer };
41
65
  //# sourceMappingURL=typeorm.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"typeorm.d.mts","names":[],"sources":["../src/adapters/typeorm.ts"],"mappings":";;;;;AAyCA;;;;;;;;;;;;;;AAAkF;;;;;;;;;;;;;;;;;;AAAlF,iBAAgB,aAAA,uBAAoC,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,gBAAA"}
1
+ {"version":3,"file":"typeorm.d.mts","names":[],"sources":["../src/adapters/typeorm.ts"],"mappings":";;;;;AAyCA;;;;;;;;;;;;;;AAAkF;AAkClF;;;;;;;;;;;;;;AAEG;;;AApCH,iBAAgB,aAAA,uBAAoC,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,gBAAA;;;;;;;;;;;;;;;;;;;;;;;;iBAkClE,qBAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,gBAAA"}
package/dist/typeorm.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
2
+ import { n as readIdColumnNullable, t as readIdColumn } from "./adapter-types-CjzFNDcJ.mjs";
3
3
  //#region src/adapters/typeorm.ts
4
4
  /**
5
5
  * TypeORM column transformer for `Id<Brand>`.
@@ -43,7 +43,40 @@ function idTransformer(codec) {
43
43
  }
44
44
  };
45
45
  }
46
+ /**
47
+ * TypeORM column transformer for a **nullable** `Id<Brand>` column.
48
+ *
49
+ * Behaves like {@link idTransformer} but `from` returns `null` for `null` /
50
+ * `undefined` database values and `to` passes `null` / `undefined` through
51
+ * unchanged. Use for optional foreign keys.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import { nullableIdTransformer } from "@smonn/ids/typeorm";
56
+ * import { createTimestampId } from "@smonn/ids";
57
+ * import type { Id } from "@smonn/ids";
58
+ * import { Column, Entity } from "typeorm";
59
+ *
60
+ * const usr = createTimestampId("usr");
61
+ *
62
+ * @Entity()
63
+ * class Post {
64
+ * @Column({ type: "text", nullable: true, transformer: nullableIdTransformer(usr) })
65
+ * authorId!: Id<"usr"> | null;
66
+ * }
67
+ * ```
68
+ */
69
+ function nullableIdTransformer(codec) {
70
+ return {
71
+ to(value) {
72
+ return value;
73
+ },
74
+ from(value) {
75
+ return readIdColumnNullable(codec, value);
76
+ }
77
+ };
78
+ }
46
79
  //#endregion
47
- export { IdsError, idTransformer, isIdsError };
80
+ export { IdsError, idTransformer, isIdsError, nullableIdTransformer };
48
81
 
49
82
  //# sourceMappingURL=typeorm.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"typeorm.mjs","names":[],"sources":["../src/adapters/typeorm.ts"],"sourcesContent":["import type { ValueTransformer } from \"typeorm\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * TypeORM column transformer for `Id<Brand>`.\n *\n * Returns a `ValueTransformer` object suitable for use in a TypeORM `@Column`\n * decorator's `transformer` option.\n *\n * **Write path (`to`):** passes the `Id<Brand>` directly to the database — it is\n * already the canonical string form.\n *\n * **Read path (`from`):** normalises the raw database value via `codec.safeParse()`.\n * Throws `IdsError` with code `\"invalid_id\"` if the value does not parse as a valid\n * `Id<Brand>`.\n *\n * **TypeORM branding caveat:** TypeORM cannot brand a generated entity field type at\n * the schema level. Annotate the entity field explicitly: `id!: Id<\"usr\">`.\n *\n * @example\n * ```ts\n * import { idTransformer } from \"@smonn/ids/typeorm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n * import { Column, Entity } from \"typeorm\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Entity()\n * class User {\n * @Column({ type: \"text\", transformer: idTransformer(usr) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer {\n return {\n to(value: Id<Brand>): string {\n return value;\n },\n from(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,cAAoC,OAA+C;CACjG,OAAO;EACL,GAAG,OAA0B;GAC3B,OAAO;EACT;EACA,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF"}
1
+ {"version":3,"file":"typeorm.mjs","names":[],"sources":["../src/adapters/typeorm.ts"],"sourcesContent":["import type { ValueTransformer } from \"typeorm\";\nimport { readIdColumn, readIdColumnNullable, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * TypeORM column transformer for `Id<Brand>`.\n *\n * Returns a `ValueTransformer` object suitable for use in a TypeORM `@Column`\n * decorator's `transformer` option.\n *\n * **Write path (`to`):** passes the `Id<Brand>` directly to the database — it is\n * already the canonical string form.\n *\n * **Read path (`from`):** normalises the raw database value via `codec.safeParse()`.\n * Throws `IdsError` with code `\"invalid_id\"` if the value does not parse as a valid\n * `Id<Brand>`.\n *\n * **TypeORM branding caveat:** TypeORM cannot brand a generated entity field type at\n * the schema level. Annotate the entity field explicitly: `id!: Id<\"usr\">`.\n *\n * @example\n * ```ts\n * import { idTransformer } from \"@smonn/ids/typeorm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n * import { Column, Entity } from \"typeorm\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Entity()\n * class User {\n * @Column({ type: \"text\", transformer: idTransformer(usr) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer {\n return {\n to(value: Id<Brand>): string {\n return value;\n },\n from(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n\n/**\n * TypeORM column transformer for a **nullable** `Id<Brand>` column.\n *\n * Behaves like {@link idTransformer} but `from` returns `null` for `null` /\n * `undefined` database values and `to` passes `null` / `undefined` through\n * unchanged. Use for optional foreign keys.\n *\n * @example\n * ```ts\n * import { nullableIdTransformer } from \"@smonn/ids/typeorm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n * import { Column, Entity } from \"typeorm\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Entity()\n * class Post {\n * @Column({ type: \"text\", nullable: true, transformer: nullableIdTransformer(usr) })\n * authorId!: Id<\"usr\"> | null;\n * }\n * ```\n */\nexport function nullableIdTransformer<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): ValueTransformer {\n return {\n to(value: Id<Brand> | null | undefined): string | null | undefined {\n return value;\n },\n from(value: unknown): Id<Brand> | null {\n return readIdColumnNullable(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,cAAoC,OAA+C;CACjG,OAAO;EACL,GAAG,OAA0B;GAC3B,OAAO;EACT;EACA,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,sBACd,OACkB;CAClB,OAAO;EACL,GAAG,OAAgE;GACjE,OAAO;EACT;EACA,KAAK,OAAkC;GACrC,OAAO,qBAAqB,OAAO,KAAK;EAC1C;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smonn/ids",
3
- "version": "0.14.1",
3
+ "version": "1.0.0-rc.0",
4
4
  "license": "MIT",
5
5
  "author": "Simon Ingeson (https://github.com/smonn)",
6
6
  "repository": {