@smonn/ids 0.15.0 → 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 (46) 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/hono.d.mts +44 -2
  19. package/dist/hono.d.mts.map +1 -1
  20. package/dist/hono.mjs +54 -2
  21. package/dist/hono.mjs.map +1 -1
  22. package/dist/kysely.d.mts +73 -2
  23. package/dist/kysely.d.mts.map +1 -1
  24. package/dist/kysely.mjs +84 -2
  25. package/dist/kysely.mjs.map +1 -1
  26. package/dist/mikro-orm.d.mts +42 -3
  27. package/dist/mikro-orm.d.mts.map +1 -1
  28. package/dist/mikro-orm.mjs +54 -4
  29. package/dist/mikro-orm.mjs.map +1 -1
  30. package/dist/nestjs.mjs +1 -1
  31. package/dist/{opaque-COAcIIY4.mjs → opaque-Dle3CmSE.mjs} +18 -10
  32. package/dist/opaque-Dle3CmSE.mjs.map +1 -0
  33. package/dist/opaque.d.mts +16 -10
  34. package/dist/opaque.d.mts.map +1 -1
  35. package/dist/opaque.mjs +1 -1
  36. package/dist/prisma.d.mts +99 -3
  37. package/dist/prisma.d.mts.map +1 -1
  38. package/dist/prisma.mjs +101 -3
  39. package/dist/prisma.mjs.map +1 -1
  40. package/dist/typeorm.d.mts +25 -1
  41. package/dist/typeorm.d.mts.map +1 -1
  42. package/dist/typeorm.mjs +35 -2
  43. package/dist/typeorm.mjs.map +1 -1
  44. package/package.json +1 -1
  45. package/dist/adapter-types-7wWdELSh.mjs.map +0 -1
  46. package/dist/opaque-COAcIIY4.mjs.map +0 -1
package/dist/hono.d.mts CHANGED
@@ -4,7 +4,7 @@ import { ContentfulStatusCode } from "hono/utils/http-status";
4
4
  import { Context, MiddlewareHandler } from "hono";
5
5
 
6
6
  //#region src/adapters/hono.d.ts
7
- /** Options for `idParam`. All fields are optional. */
7
+ /** Options for `idParam` and `idQuery`. All fields are optional. */
8
8
  type IdParamOptions = {
9
9
  /**
10
10
  * Called instead of throwing when provided. The hook owns the response entirely —
@@ -61,6 +61,48 @@ type IdParamOptions = {
61
61
  declare function idParam<ParamKey extends string, Brand extends string>(paramName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): MiddlewareHandler<{
62
62
  Variables: Record<ParamKey, Id<Brand>>;
63
63
  }>;
64
+ /**
65
+ * Hono middleware that validates a named query-string param against a codec via `safeParse`.
66
+ *
67
+ * Same failure contract as `idParam` — same `IdParamFailure` shape, same `onError` / `status`
68
+ * options — but reads `c.req.query(queryName)` instead of `c.req.param(queryName)`.
69
+ *
70
+ * **Default (no options):** throws `HTTPException(status)` so the app's existing `onError` handler
71
+ * controls rendering and content negotiation. The adapter does not write a response body itself.
72
+ *
73
+ * **`options.onError`:** when provided, the hook owns the response entirely — the adapter neither
74
+ * throws nor writes a response.
75
+ *
76
+ * **`options.status`:** remaps the default HTTP status for a reason without a full handler.
77
+ *
78
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
79
+ * - **Malformed or missing query param → `reason: "malformed"`, default 400**
80
+ *
81
+ * On success, stores the canonical `Id<Brand>` in the Hono context under `queryName`
82
+ * and calls `next()`.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * import { idQuery } from "@smonn/ids/hono";
87
+ * import { createTimestampId } from "@smonn/ids";
88
+ *
89
+ * const usr = createTimestampId("usr");
90
+ *
91
+ * // Default: throws HTTPException → app.onError renders it
92
+ * // GET /users?userId=usr_...
93
+ * app.get("/users", idQuery("userId", usr), (c) => {
94
+ * const userId = c.get("userId"); // Id<"usr">, canonical
95
+ * });
96
+ *
97
+ * // Override: consumer fully owns the response
98
+ * app.get("/search", idQuery("cursor", usr, {
99
+ * onError: (failure, c) => c.json({ error: failure.reason }, failure.status),
100
+ * }), handler);
101
+ * ```
102
+ */
103
+ declare function idQuery<ParamKey extends string, Brand extends string>(queryName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): MiddlewareHandler<{
104
+ Variables: Record<ParamKey, Id<Brand>>;
105
+ }>;
64
106
  //#endregion
65
- export { type IdParamFailure, IdParamOptions, idParam };
107
+ export { type IdParamFailure, IdParamOptions, idParam, idQuery };
66
108
  //# sourceMappingURL=hono.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hono.d.mts","names":[],"sources":["../src/adapters/hono.ts"],"mappings":";;;;;;;KASY,cAAA;EAAA;;;;EAKV,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,CAAA,EAAG,OAAA,KAAY,QAAA,GAAW,OAAA,CAAQ,QAAA;;;;;EAKtE,MAAA;IAAW,cAAA,GAAiB,oBAAA;IAAsB,SAAA,GAAY,oBAAA;EAAA;AAAA;;;;;;;;;;;;AAAA;AAyChE;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,GACT,iBAAA;EAAoB,SAAA,EAAW,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA;AAAA"}
1
+ {"version":3,"file":"hono.d.mts","names":[],"sources":["../src/adapters/hono.ts"],"mappings":";;;;;;;KASY,cAAA;EAAA;;;;EAKV,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,CAAA,EAAG,OAAA,KAAY,QAAA,GAAW,OAAA,CAAQ,QAAA;;;;;EAKtE,MAAA;IAAW,cAAA,GAAiB,oBAAA;IAAsB,SAAA,GAAY,oBAAA;EAAA;AAAA;;;;;;;;;;;;AAAA;AAyChE;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,GACT,iBAAA;EAAoB,SAAA,EAAW,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA;;;;;;;;;;iBAJtC,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,GACT,iBAAA;EAAoB,SAAA,EAAW,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA;AAAA"}
package/dist/hono.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
1
+ import { r as resolveIdParamFailure } from "./adapter-types-CjzFNDcJ.mjs";
2
2
  import { HTTPException } from "hono/http-exception";
3
3
  //#region src/adapters/hono.ts
4
4
  /**
@@ -52,7 +52,59 @@ function idParam(paramName, codec, options) {
52
52
  await next();
53
53
  };
54
54
  }
55
+ /**
56
+ * Hono middleware that validates a named query-string param against a codec via `safeParse`.
57
+ *
58
+ * Same failure contract as `idParam` — same `IdParamFailure` shape, same `onError` / `status`
59
+ * options — but reads `c.req.query(queryName)` instead of `c.req.param(queryName)`.
60
+ *
61
+ * **Default (no options):** throws `HTTPException(status)` so the app's existing `onError` handler
62
+ * controls rendering and content negotiation. The adapter does not write a response body itself.
63
+ *
64
+ * **`options.onError`:** when provided, the hook owns the response entirely — the adapter neither
65
+ * throws nor writes a response.
66
+ *
67
+ * **`options.status`:** remaps the default HTTP status for a reason without a full handler.
68
+ *
69
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
70
+ * - **Malformed or missing query param → `reason: "malformed"`, default 400**
71
+ *
72
+ * On success, stores the canonical `Id<Brand>` in the Hono context under `queryName`
73
+ * and calls `next()`.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { idQuery } from "@smonn/ids/hono";
78
+ * import { createTimestampId } from "@smonn/ids";
79
+ *
80
+ * const usr = createTimestampId("usr");
81
+ *
82
+ * // Default: throws HTTPException → app.onError renders it
83
+ * // GET /users?userId=usr_...
84
+ * app.get("/users", idQuery("userId", usr), (c) => {
85
+ * const userId = c.get("userId"); // Id<"usr">, canonical
86
+ * });
87
+ *
88
+ * // Override: consumer fully owns the response
89
+ * app.get("/search", idQuery("cursor", usr, {
90
+ * onError: (failure, c) => c.json({ error: failure.reason }, failure.status),
91
+ * }), handler);
92
+ * ```
93
+ */
94
+ function idQuery(queryName, codec, options) {
95
+ return async (c, next) => {
96
+ const raw = c.req.query(queryName);
97
+ const result = codec.safeParse(raw);
98
+ if (!result.ok) {
99
+ const failure = resolveIdParamFailure(result.error, options);
100
+ if (options?.onError) return options.onError(failure, c);
101
+ throw new HTTPException(failure.status);
102
+ }
103
+ c.set(queryName, result.id);
104
+ await next();
105
+ };
106
+ }
55
107
  //#endregion
56
- export { idParam };
108
+ export { idParam, idQuery };
57
109
 
58
110
  //# sourceMappingURL=hono.mjs.map
package/dist/hono.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"hono.mjs","names":[],"sources":["../src/adapters/hono.ts"],"sourcesContent":["import { HTTPException } from \"hono/http-exception\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/** Options for `idParam`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of throwing when provided. The hook owns the response entirely —\n * the adapter neither throws nor writes a body.\n */\n onError?: (failure: IdParamFailure, c: Context) => Response | Promise<Response>;\n /**\n * Remap the default HTTP status for a failure reason without a full handler.\n * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.\n */\n status?: { brand_mismatch?: ContentfulStatusCode; malformed?: ContentfulStatusCode };\n};\n\n/**\n * Hono middleware that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** throws `HTTPException(status)` so the app's existing `onError` handler\n * controls rendering and content negotiation. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter neither\n * throws nor writes a response.\n *\n * **`options.status`:** remaps the default HTTP status for a reason without a full handler.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing ID → `reason: \"malformed\"`, default 400**\n *\n * On success, stores the canonical `Id<Brand>` in the Hono context under `paramName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idParam } from \"@smonn/ids/hono\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws HTTPException → app.onError renders it\n * app.get(\"/users/:id\", idParam(\"id\", usr), (c) => {\n * const id = c.get(\"id\"); // Id<\"usr\">, canonical\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/orgs/:id\", idParam(\"id\", org, {\n * onError: (failure, c) => c.json({ error: failure.reason }, failure.status),\n * }), handler);\n *\n * // Or a lightweight status remap without a full handler\n * app.get(\"/things/:id\", idParam(\"id\", thing, { status: { brand_mismatch: 400 } }), handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): MiddlewareHandler<{ Variables: Record<ParamKey, Id<Brand>> }> {\n return async (c, next) => {\n const raw = c.req.param(paramName);\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n return options.onError(failure, c);\n }\n throw new HTTPException(failure.status as ContentfulStatusCode);\n }\n c.set(paramName, result.id);\n await next();\n return;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAgB,QACd,WACA,OACA,SAC+D;CAC/D,OAAO,OAAO,GAAG,SAAS;EACxB,MAAM,MAAM,EAAE,IAAI,MAAM,SAAS;EACjC,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SACX,OAAO,QAAQ,QAAQ,SAAS,CAAC;GAEnC,MAAM,IAAI,cAAc,QAAQ,MAA8B;EAChE;EACA,EAAE,IAAI,WAAW,OAAO,EAAE;EAC1B,MAAM,KAAK;CAEb;AACF"}
1
+ {"version":3,"file":"hono.mjs","names":[],"sources":["../src/adapters/hono.ts"],"sourcesContent":["import { HTTPException } from \"hono/http-exception\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/** Options for `idParam` and `idQuery`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of throwing when provided. The hook owns the response entirely —\n * the adapter neither throws nor writes a body.\n */\n onError?: (failure: IdParamFailure, c: Context) => Response | Promise<Response>;\n /**\n * Remap the default HTTP status for a failure reason without a full handler.\n * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.\n */\n status?: { brand_mismatch?: ContentfulStatusCode; malformed?: ContentfulStatusCode };\n};\n\n/**\n * Hono middleware that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** throws `HTTPException(status)` so the app's existing `onError` handler\n * controls rendering and content negotiation. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter neither\n * throws nor writes a response.\n *\n * **`options.status`:** remaps the default HTTP status for a reason without a full handler.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing ID → `reason: \"malformed\"`, default 400**\n *\n * On success, stores the canonical `Id<Brand>` in the Hono context under `paramName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idParam } from \"@smonn/ids/hono\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws HTTPException → app.onError renders it\n * app.get(\"/users/:id\", idParam(\"id\", usr), (c) => {\n * const id = c.get(\"id\"); // Id<\"usr\">, canonical\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/orgs/:id\", idParam(\"id\", org, {\n * onError: (failure, c) => c.json({ error: failure.reason }, failure.status),\n * }), handler);\n *\n * // Or a lightweight status remap without a full handler\n * app.get(\"/things/:id\", idParam(\"id\", thing, { status: { brand_mismatch: 400 } }), handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): MiddlewareHandler<{ Variables: Record<ParamKey, Id<Brand>> }> {\n return async (c, next) => {\n const raw = c.req.param(paramName);\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n return options.onError(failure, c);\n }\n throw new HTTPException(failure.status as ContentfulStatusCode);\n }\n c.set(paramName, result.id);\n await next();\n return;\n };\n}\n\n/**\n * Hono middleware that validates a named query-string param against a codec via `safeParse`.\n *\n * Same failure contract as `idParam` — same `IdParamFailure` shape, same `onError` / `status`\n * options — but reads `c.req.query(queryName)` instead of `c.req.param(queryName)`.\n *\n * **Default (no options):** throws `HTTPException(status)` so the app's existing `onError` handler\n * controls rendering and content negotiation. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter neither\n * throws nor writes a response.\n *\n * **`options.status`:** remaps the default HTTP status for a reason without a full handler.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing query param → `reason: \"malformed\"`, default 400**\n *\n * On success, stores the canonical `Id<Brand>` in the Hono context under `queryName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idQuery } from \"@smonn/ids/hono\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws HTTPException → app.onError renders it\n * // GET /users?userId=usr_...\n * app.get(\"/users\", idQuery(\"userId\", usr), (c) => {\n * const userId = c.get(\"userId\"); // Id<\"usr\">, canonical\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/search\", idQuery(\"cursor\", usr, {\n * onError: (failure, c) => c.json({ error: failure.reason }, failure.status),\n * }), handler);\n * ```\n */\nexport function idQuery<ParamKey extends string, Brand extends string>(\n queryName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): MiddlewareHandler<{ Variables: Record<ParamKey, Id<Brand>> }> {\n return async (c, next) => {\n const raw = c.req.query(queryName);\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n return options.onError(failure, c);\n }\n throw new HTTPException(failure.status as ContentfulStatusCode);\n }\n c.set(queryName, result.id);\n await next();\n return;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAgB,QACd,WACA,OACA,SAC+D;CAC/D,OAAO,OAAO,GAAG,SAAS;EACxB,MAAM,MAAM,EAAE,IAAI,MAAM,SAAS;EACjC,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SACX,OAAO,QAAQ,QAAQ,SAAS,CAAC;GAEnC,MAAM,IAAI,cAAc,QAAQ,MAA8B;EAChE;EACA,EAAE,IAAI,WAAW,OAAO,EAAE;EAC1B,MAAM,KAAK;CAEb;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,QACd,WACA,OACA,SAC+D;CAC/D,OAAO,OAAO,GAAG,SAAS;EACxB,MAAM,MAAM,EAAE,IAAI,MAAM,SAAS;EACjC,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SACX,OAAO,QAAQ,QAAQ,SAAS,CAAC;GAEnC,MAAM,IAAI,cAAc,QAAQ,MAA8B;EAChE;EACA,EAAE,IAAI,WAAW,OAAO,EAAE;EAC1B,MAAM,KAAK;CAEb;AACF"}
package/dist/kysely.d.mts CHANGED
@@ -1,7 +1,7 @@
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 { ColumnType } from "kysely";
4
+ import { ColumnType, KyselyPlugin } from "kysely";
5
5
 
6
6
  //#region src/adapters/kysely.d.ts
7
7
  /**
@@ -23,6 +23,24 @@ import { ColumnType } from "kysely";
23
23
  */
24
24
  type IdColumnType<Brand extends string> = ColumnType<Id<Brand>, Id<Brand>, Id<Brand>>;
25
25
  /**
26
+ * Kysely column type mapping for a nullable `Id<Brand>` column.
27
+ *
28
+ * Use in your Kysely `Database` interface for optional foreign keys or any
29
+ * column that can be `NULL`. Pair with `nullableIdColumn(codec)` for runtime
30
+ * transformation.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import type { NullableIdColumnType } from "@smonn/ids/kysely";
35
+ * import type { Id } from "@smonn/ids";
36
+ *
37
+ * interface Database {
38
+ * posts: { authorId: NullableIdColumnType<"usr"> };
39
+ * }
40
+ * ```
41
+ */
42
+ type NullableIdColumnType<Brand extends string> = ColumnType<Id<Brand> | null, Id<Brand> | null, Id<Brand> | null>;
43
+ /**
26
44
  * Kysely column adapter bound to a codec.
27
45
  *
28
46
  * Returns an object with `fromDriver` / `toDriver` helpers that mirror the read/write
@@ -51,6 +69,59 @@ declare function idColumn<Brand extends string>(codec: IdColumnCodec<Brand>): {
51
69
  toDriver(value: Id<Brand>): string;
52
70
  fromDriver(value: string): Id<Brand>;
53
71
  };
72
+ /**
73
+ * Kysely plugin that automatically transforms result columns using the provided codec map.
74
+ *
75
+ * Keys in `map` are plain column names (`"id"`) or `"table.column"` qualified names
76
+ * (`"users.id"`). Plain names match any result column with that name; qualified names
77
+ * match by the column-name part (after the last `.`). If both a plain key and a qualified
78
+ * key resolve to the same column name, the qualified key takes precedence.
79
+ *
80
+ * `transformResult` calls `readIdColumn(codec, rawValue)` for each matched column, returning
81
+ * a branded `Id<Brand>` on success and throwing `IdsError("invalid_id")` on parse failure.
82
+ * `transformQuery` is a no-op identity pass-through.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * import { idPlugin } from "@smonn/ids/kysely";
87
+ * import { createTimestampId } from "@smonn/ids";
88
+ * import { Kysely } from "kysely";
89
+ *
90
+ * const usr = createTimestampId("usr");
91
+ *
92
+ * const db = new Kysely<Database>({
93
+ * // ...
94
+ * plugins: [idPlugin({ "users.id": usr })],
95
+ * });
96
+ *
97
+ * // result.id is automatically validated and branded as Id<"usr">
98
+ * const row = await db.selectFrom("users").selectAll().executeTakeFirstOrThrow();
99
+ * ```
100
+ */
101
+ declare function idPlugin(map: Record<string, IdColumnCodec<string>>): KyselyPlugin;
102
+ /**
103
+ * Kysely column adapter for a **nullable** `Id<Brand>` column.
104
+ *
105
+ * Behaves like {@link idColumn} but `fromDriver` returns `null` for `null` /
106
+ * `undefined` driver values and `toDriver` passes `null` through unchanged.
107
+ * Use for optional foreign keys and `LEFT JOIN` results.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { nullableIdColumn } from "@smonn/ids/kysely";
112
+ * import { createTimestampId } from "@smonn/ids";
113
+ *
114
+ * const usr = createTimestampId("usr");
115
+ * const authorCol = nullableIdColumn(usr);
116
+ *
117
+ * // In a query result handler:
118
+ * const authorId = authorCol.fromDriver(row.author_id); // Id<"usr"> | null
119
+ * ```
120
+ */
121
+ declare function nullableIdColumn<Brand extends string>(codec: IdColumnCodec<Brand>): {
122
+ toDriver(value: Id<Brand> | null): string | null;
123
+ fromDriver(value: string | null): Id<Brand> | null;
124
+ };
54
125
  //#endregion
55
- export { type IdColumnCodec, IdColumnType, IdsError, type IdsErrorCode, idColumn, isIdsError };
126
+ export { type IdColumnCodec, IdColumnType, IdsError, type IdsErrorCode, NullableIdColumnType, idColumn, idPlugin, isIdsError, nullableIdColumn };
56
127
  //# sourceMappingURL=kysely.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"kysely.d.mts","names":[],"sources":["../src/adapters/kysely.ts"],"mappings":";;;;;;AAyBA;;;;;;;;;;;;;;;;;AAAA,KAAY,YAAA,yBAAqC,UAAA,CAAW,EAAA,CAAG,KAAA,GAAQ,EAAA,CAAG,KAAA,GAAQ,EAAA,CAAG,KAAA;;;;AAAA;AA2BrF;;;;;;;;;;;;;;;;;;;;;iBAAgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA;EAErB,QAAA,CAAS,KAAA,EAAO,EAAA,CAAG,KAAA;EACnB,UAAA,CAAW,KAAA,WAAgB,EAAA,CAAG,KAAA;AAAA"}
1
+ {"version":3,"file":"kysely.d.mts","names":[],"sources":["../src/adapters/kysely.ts"],"mappings":";;;;;;AAgCA;;;;;;;;;;;;;;;;;AAAA,KAAY,YAAA,yBAAqC,UAAA,CAAW,EAAA,CAAG,KAAA,GAAQ,EAAA,CAAG,KAAA,GAAQ,EAAA,CAAG,KAAA;;;;AAAA;AAmBrF;;;;;;;;;;;;;KAAY,oBAAA,yBAA6C,UAAA,CACvD,EAAA,CAAG,KAAA,UACH,EAAA,CAAG,KAAA,UACH,EAAA,CAAG,KAAA;;;;;;;;AAAA;AA4BL;;;;;;;;;;;;;;;;;iBAAgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA;EAErB,QAAA,CAAS,KAAA,EAAO,EAAA,CAAG,KAAA;EACnB,UAAA,CAAW,KAAA,WAAgB,EAAA,CAAG,KAAA;AAAA;;;;AAAA;AAyChC;;;;;;;;;;;;;AAAsE;AAqDtE;;;;;;;;;;;iBArDgB,QAAA,CAAS,GAAA,EAAK,MAAA,SAAe,aAAA,YAAyB,YAAA;;;;;;;;;;;;;AAyD/B;;;;;;;iBAJvB,gBAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA;EAErB,QAAA,CAAS,KAAA,EAAO,EAAA,CAAG,KAAA;EACnB,UAAA,CAAW,KAAA,kBAAuB,EAAA,CAAG,KAAA;AAAA"}
package/dist/kysely.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/kysely.ts
4
4
  /**
5
5
  * Kysely column adapter bound to a codec.
@@ -36,7 +36,89 @@ function idColumn(codec) {
36
36
  }
37
37
  };
38
38
  }
39
+ /**
40
+ * Kysely plugin that automatically transforms result columns using the provided codec map.
41
+ *
42
+ * Keys in `map` are plain column names (`"id"`) or `"table.column"` qualified names
43
+ * (`"users.id"`). Plain names match any result column with that name; qualified names
44
+ * match by the column-name part (after the last `.`). If both a plain key and a qualified
45
+ * key resolve to the same column name, the qualified key takes precedence.
46
+ *
47
+ * `transformResult` calls `readIdColumn(codec, rawValue)` for each matched column, returning
48
+ * a branded `Id<Brand>` on success and throwing `IdsError("invalid_id")` on parse failure.
49
+ * `transformQuery` is a no-op identity pass-through.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { idPlugin } from "@smonn/ids/kysely";
54
+ * import { createTimestampId } from "@smonn/ids";
55
+ * import { Kysely } from "kysely";
56
+ *
57
+ * const usr = createTimestampId("usr");
58
+ *
59
+ * const db = new Kysely<Database>({
60
+ * // ...
61
+ * plugins: [idPlugin({ "users.id": usr })],
62
+ * });
63
+ *
64
+ * // result.id is automatically validated and branded as Id<"usr">
65
+ * const row = await db.selectFrom("users").selectAll().executeTakeFirstOrThrow();
66
+ * ```
67
+ */
68
+ function idPlugin(map) {
69
+ const lookup = /* @__PURE__ */ new Map();
70
+ for (const [key, codec] of Object.entries(map)) if (!key.includes(".")) lookup.set(key, codec);
71
+ for (const [key, codec] of Object.entries(map)) if (key.includes(".")) lookup.set(key.slice(key.lastIndexOf(".") + 1), codec);
72
+ return {
73
+ transformQuery(args) {
74
+ return args.node;
75
+ },
76
+ async transformResult(args) {
77
+ const rows = args.result.rows.map((row) => {
78
+ const newRow = {};
79
+ for (const [colName, value] of Object.entries(row)) {
80
+ const codec = lookup.get(colName);
81
+ newRow[colName] = codec !== void 0 ? readIdColumn(codec, value) : value;
82
+ }
83
+ return newRow;
84
+ });
85
+ return {
86
+ ...args.result,
87
+ rows
88
+ };
89
+ }
90
+ };
91
+ }
92
+ /**
93
+ * Kysely column adapter for a **nullable** `Id<Brand>` column.
94
+ *
95
+ * Behaves like {@link idColumn} but `fromDriver` returns `null` for `null` /
96
+ * `undefined` driver values and `toDriver` passes `null` through unchanged.
97
+ * Use for optional foreign keys and `LEFT JOIN` results.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { nullableIdColumn } from "@smonn/ids/kysely";
102
+ * import { createTimestampId } from "@smonn/ids";
103
+ *
104
+ * const usr = createTimestampId("usr");
105
+ * const authorCol = nullableIdColumn(usr);
106
+ *
107
+ * // In a query result handler:
108
+ * const authorId = authorCol.fromDriver(row.author_id); // Id<"usr"> | null
109
+ * ```
110
+ */
111
+ function nullableIdColumn(codec) {
112
+ return {
113
+ toDriver(value) {
114
+ return value;
115
+ },
116
+ fromDriver(value) {
117
+ return readIdColumnNullable(codec, value);
118
+ }
119
+ };
120
+ }
39
121
  //#endregion
40
- export { IdsError, idColumn, isIdsError };
122
+ export { IdsError, idColumn, idPlugin, isIdsError, nullableIdColumn };
41
123
 
42
124
  //# sourceMappingURL=kysely.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"kysely.mjs","names":[],"sources":["../src/adapters/kysely.ts"],"sourcesContent":["import type { ColumnType } from \"kysely\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdColumnCodec } from \"./adapter-types.js\";\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\n/**\n * Kysely column type mapping for `Id<Brand>`.\n *\n * Use this in your Kysely `Database` interface to type a column as `Id<Brand>` at\n * the TypeScript level. Pair it with `idColumn(codec)` for runtime read/write\n * transformation.\n *\n * @example\n * ```ts\n * import type { IdColumnType } from \"@smonn/ids/kysely\";\n * import type { Id } from \"@smonn/ids\";\n *\n * interface Database {\n * users: { id: IdColumnType<\"usr\"> };\n * }\n * ```\n */\nexport type IdColumnType<Brand extends string> = ColumnType<Id<Brand>, Id<Brand>, Id<Brand>>;\n\n/**\n * Kysely column adapter bound to a codec.\n *\n * Returns an object with `fromDriver` / `toDriver` helpers that mirror the read/write\n * contract of the Drizzle adapter — same error message, same strictness (safeParse on\n * read, identity on write).\n *\n * **Write path:** passes the `Id<Brand>` directly to the driver — it is already\n * the canonical string form.\n *\n * **Read path:** normalises the raw DB string via `codec.safeParse()`. Throws if\n * the value does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/kysely\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const usrCol = idColumn(usr);\n *\n * // In a query result handler:\n * const id = usrCol.fromDriver(row.id);\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): {\n toDriver(value: Id<Brand>): string;\n fromDriver(value: string): Id<Brand>;\n} {\n return {\n toDriver(value: Id<Brand>): string {\n return value;\n },\n fromDriver(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,SACd,OAIA;CACA,OAAO;EACL,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF"}
1
+ {"version":3,"file":"kysely.mjs","names":[],"sources":["../src/adapters/kysely.ts"],"sourcesContent":["import type {\n ColumnType,\n KyselyPlugin,\n PluginTransformQueryArgs,\n PluginTransformResultArgs,\n QueryResult,\n UnknownRow,\n} from \"kysely\";\nimport { readIdColumn, readIdColumnNullable, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdColumnCodec } from \"./adapter-types.js\";\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\n/**\n * Kysely column type mapping for `Id<Brand>`.\n *\n * Use this in your Kysely `Database` interface to type a column as `Id<Brand>` at\n * the TypeScript level. Pair it with `idColumn(codec)` for runtime read/write\n * transformation.\n *\n * @example\n * ```ts\n * import type { IdColumnType } from \"@smonn/ids/kysely\";\n * import type { Id } from \"@smonn/ids\";\n *\n * interface Database {\n * users: { id: IdColumnType<\"usr\"> };\n * }\n * ```\n */\nexport type IdColumnType<Brand extends string> = ColumnType<Id<Brand>, Id<Brand>, Id<Brand>>;\n\n/**\n * Kysely column type mapping for a nullable `Id<Brand>` column.\n *\n * Use in your Kysely `Database` interface for optional foreign keys or any\n * column that can be `NULL`. Pair with `nullableIdColumn(codec)` for runtime\n * transformation.\n *\n * @example\n * ```ts\n * import type { NullableIdColumnType } from \"@smonn/ids/kysely\";\n * import type { Id } from \"@smonn/ids\";\n *\n * interface Database {\n * posts: { authorId: NullableIdColumnType<\"usr\"> };\n * }\n * ```\n */\nexport type NullableIdColumnType<Brand extends string> = ColumnType<\n Id<Brand> | null,\n Id<Brand> | null,\n Id<Brand> | null\n>;\n\n/**\n * Kysely column adapter bound to a codec.\n *\n * Returns an object with `fromDriver` / `toDriver` helpers that mirror the read/write\n * contract of the Drizzle adapter — same error message, same strictness (safeParse on\n * read, identity on write).\n *\n * **Write path:** passes the `Id<Brand>` directly to the driver — it is already\n * the canonical string form.\n *\n * **Read path:** normalises the raw DB string via `codec.safeParse()`. Throws if\n * the value does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/kysely\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const usrCol = idColumn(usr);\n *\n * // In a query result handler:\n * const id = usrCol.fromDriver(row.id);\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): {\n toDriver(value: Id<Brand>): string;\n fromDriver(value: string): Id<Brand>;\n} {\n return {\n toDriver(value: Id<Brand>): string {\n return value;\n },\n fromDriver(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n\n/**\n * Kysely plugin that automatically transforms result columns using the provided codec map.\n *\n * Keys in `map` are plain column names (`\"id\"`) or `\"table.column\"` qualified names\n * (`\"users.id\"`). Plain names match any result column with that name; qualified names\n * match by the column-name part (after the last `.`). If both a plain key and a qualified\n * key resolve to the same column name, the qualified key takes precedence.\n *\n * `transformResult` calls `readIdColumn(codec, rawValue)` for each matched column, returning\n * a branded `Id<Brand>` on success and throwing `IdsError(\"invalid_id\")` on parse failure.\n * `transformQuery` is a no-op identity pass-through.\n *\n * @example\n * ```ts\n * import { idPlugin } from \"@smonn/ids/kysely\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import { Kysely } from \"kysely\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * const db = new Kysely<Database>({\n * // ...\n * plugins: [idPlugin({ \"users.id\": usr })],\n * });\n *\n * // result.id is automatically validated and branded as Id<\"usr\">\n * const row = await db.selectFrom(\"users\").selectAll().executeTakeFirstOrThrow();\n * ```\n */\nexport function idPlugin(map: Record<string, IdColumnCodec<string>>): KyselyPlugin {\n // Build a lookup keyed by the bare column name.\n // Plain keys (\"id\") are added first; qualified keys (\"users.id\") are added second\n // so they override the plain key when both resolve to the same column name.\n const lookup = new Map<string, IdColumnCodec<string>>();\n for (const [key, codec] of Object.entries(map)) {\n if (!key.includes(\".\")) {\n lookup.set(key, codec);\n }\n }\n for (const [key, codec] of Object.entries(map)) {\n if (key.includes(\".\")) {\n lookup.set(key.slice(key.lastIndexOf(\".\") + 1), codec);\n }\n }\n\n return {\n transformQuery(args: PluginTransformQueryArgs) {\n return args.node;\n },\n async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {\n const rows = args.result.rows.map((row) => {\n const newRow: Record<string, unknown> = {};\n for (const [colName, value] of Object.entries(row)) {\n const codec = lookup.get(colName);\n newRow[colName] = codec !== undefined ? readIdColumn(codec, value) : value;\n }\n return newRow;\n });\n return { ...args.result, rows };\n },\n };\n}\n\n/**\n * Kysely column adapter for a **nullable** `Id<Brand>` column.\n *\n * Behaves like {@link idColumn} but `fromDriver` returns `null` for `null` /\n * `undefined` driver values and `toDriver` passes `null` through unchanged.\n * Use for optional foreign keys and `LEFT JOIN` results.\n *\n * @example\n * ```ts\n * import { nullableIdColumn } from \"@smonn/ids/kysely\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const authorCol = nullableIdColumn(usr);\n *\n * // In a query result handler:\n * const authorId = authorCol.fromDriver(row.author_id); // Id<\"usr\"> | null\n * ```\n */\nexport function nullableIdColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): {\n toDriver(value: Id<Brand> | null): string | null;\n fromDriver(value: string | null): Id<Brand> | null;\n} {\n return {\n toDriver(value: Id<Brand> | null): string | null {\n return value;\n },\n fromDriver(value: string | null): Id<Brand> | null {\n return readIdColumnNullable(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFA,SAAgB,SACd,OAIA;CACA,OAAO;EACL,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,SAAS,KAA0D;CAIjF,MAAM,yBAAS,IAAI,IAAmC;CACtD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAC3C,IAAI,CAAC,IAAI,SAAS,GAAG,GACnB,OAAO,IAAI,KAAK,KAAK;CAGzB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAC3C,IAAI,IAAI,SAAS,GAAG,GAClB,OAAO,IAAI,IAAI,MAAM,IAAI,YAAY,GAAG,IAAI,CAAC,GAAG,KAAK;CAIzD,OAAO;EACL,eAAe,MAAgC;GAC7C,OAAO,KAAK;EACd;EACA,MAAM,gBAAgB,MAAmE;GACvF,MAAM,OAAO,KAAK,OAAO,KAAK,KAAK,QAAQ;IACzC,MAAM,SAAkC,CAAC;IACzC,KAAK,MAAM,CAAC,SAAS,UAAU,OAAO,QAAQ,GAAG,GAAG;KAClD,MAAM,QAAQ,OAAO,IAAI,OAAO;KAChC,OAAO,WAAW,UAAU,KAAA,IAAY,aAAa,OAAO,KAAK,IAAI;IACvE;IACA,OAAO;GACT,CAAC;GACD,OAAO;IAAE,GAAG,KAAK;IAAQ;GAAK;EAChC;CACF;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,iBACd,OAIA;CACA,OAAO;EACL,SAAS,OAAwC;GAC/C,OAAO;EACT;EACA,WAAW,OAAwC;GACjD,OAAO,qBAAqB,OAAO,KAAK;EAC1C;CACF;AACF"}
@@ -14,7 +14,14 @@ import { Type } from "@mikro-orm/core";
14
14
  * `codec.safeParse()`. Throws `IdsError("invalid_id")` if the stored value
15
15
  * does not parse as a valid `Id<Brand>`.
16
16
  *
17
- * **Column type** (`getColumnType`): returns `"text"`.
17
+ * **Column type** (`getColumnType`): returns `"text"` by default, or the
18
+ * `options.columnType` override when provided.
19
+ *
20
+ * @param codec - The brand-scoped codec used to parse values read from the database.
21
+ * @param options - Optional column configuration.
22
+ * @param options.columnType - SQL column type to use (default: `"text"`). Pass
23
+ * `"varchar(30)"` or `"char(26)"` to match an existing DDL or index strategy.
24
+ * The value is passed through verbatim — no validation is performed.
18
25
  *
19
26
  * @example
20
27
  * ```ts
@@ -25,13 +32,45 @@ import { Type } from "@mikro-orm/core";
25
32
  *
26
33
  * const usr = createTimestampId("usr");
27
34
  *
35
+ * // default: text column
28
36
  * class User {
29
37
  * @PrimaryKey({ type: idType(usr) })
30
38
  * id!: Id<"usr">;
31
39
  * }
40
+ *
41
+ * // explicit varchar column
42
+ * class Org {
43
+ * @PrimaryKey({ type: idType(usr, { columnType: "varchar(30)" }) })
44
+ * id!: Id<"usr">;
45
+ * }
46
+ * ```
47
+ */
48
+ declare function idType<Brand extends string>(codec: IdColumnCodec<Brand>, options?: {
49
+ columnType?: string;
50
+ }): new () => Type<Id<Brand>, string>;
51
+ /**
52
+ * Factory that returns a MikroORM `Type` subclass for a **nullable** `Id<Brand>` column.
53
+ *
54
+ * Behaves like {@link idType} but `convertToJSValue` returns `null` for `null` /
55
+ * `undefined` database values and `convertToDatabaseValue` passes `null` through
56
+ * unchanged. Use for optional foreign keys.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { Property } from "@mikro-orm/core";
61
+ * import { nullableIdType } from "@smonn/ids/mikro-orm";
62
+ * import { createTimestampId } from "@smonn/ids";
63
+ * import type { Id } from "@smonn/ids";
64
+ *
65
+ * const usr = createTimestampId("usr");
66
+ *
67
+ * class Post {
68
+ * @Property({ type: nullableIdType(usr), nullable: true })
69
+ * authorId!: Id<"usr"> | null;
70
+ * }
32
71
  * ```
33
72
  */
34
- declare function idType<Brand extends string>(codec: IdColumnCodec<Brand>): new () => Type<Id<Brand>, string>;
73
+ declare function nullableIdType<Brand extends string>(codec: IdColumnCodec<Brand>): new () => Type<Id<Brand> | null, string | null>;
35
74
  //#endregion
36
- export { type IdColumnCodec, IdsError, type IdsErrorCode, idType, isIdsError };
75
+ export { type IdColumnCodec, IdsError, type IdsErrorCode, idType, isIdsError, nullableIdType };
37
76
  //# sourceMappingURL=mikro-orm.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mikro-orm.d.mts","names":[],"sources":["../src/adapters/mikro-orm.ts"],"mappings":";;;;;;AAoCA;;;;;;;;;;;;;;;;;;AAEqB;;;;;;;;;AAFrB,iBAAgB,MAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,cACV,IAAA,CAAK,EAAA,CAAG,KAAA"}
1
+ {"version":3,"file":"mikro-orm.d.mts","names":[],"sources":["../src/adapters/mikro-orm.ts"],"mappings":";;;;;;AAkDA;;;;;;;;;;;;;;;;;;;;AAGqB;AAqCrB;;;;;;;;;;;;;;;;;;AAEqB;;AA1CrB,iBAAgB,MAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,GACrB,OAAA;EAAY,UAAA;AAAA,cACD,IAAA,CAAK,EAAA,CAAG,KAAA;;;;;;;;;;;;;;;;;;;;;;;iBAqCL,cAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,cACV,IAAA,CAAK,EAAA,CAAG,KAAA"}
@@ -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
  import { Type } from "@mikro-orm/core";
4
4
  //#region src/adapters/mikro-orm.ts
5
5
  /**
@@ -12,7 +12,14 @@ import { Type } from "@mikro-orm/core";
12
12
  * `codec.safeParse()`. Throws `IdsError("invalid_id")` if the stored value
13
13
  * does not parse as a valid `Id<Brand>`.
14
14
  *
15
- * **Column type** (`getColumnType`): returns `"text"`.
15
+ * **Column type** (`getColumnType`): returns `"text"` by default, or the
16
+ * `options.columnType` override when provided.
17
+ *
18
+ * @param codec - The brand-scoped codec used to parse values read from the database.
19
+ * @param options - Optional column configuration.
20
+ * @param options.columnType - SQL column type to use (default: `"text"`). Pass
21
+ * `"varchar(30)"` or `"char(26)"` to match an existing DDL or index strategy.
22
+ * The value is passed through verbatim — no validation is performed.
16
23
  *
17
24
  * @example
18
25
  * ```ts
@@ -23,13 +30,21 @@ import { Type } from "@mikro-orm/core";
23
30
  *
24
31
  * const usr = createTimestampId("usr");
25
32
  *
33
+ * // default: text column
26
34
  * class User {
27
35
  * @PrimaryKey({ type: idType(usr) })
28
36
  * id!: Id<"usr">;
29
37
  * }
38
+ *
39
+ * // explicit varchar column
40
+ * class Org {
41
+ * @PrimaryKey({ type: idType(usr, { columnType: "varchar(30)" }) })
42
+ * id!: Id<"usr">;
43
+ * }
30
44
  * ```
31
45
  */
32
- function idType(codec) {
46
+ function idType(codec, options) {
47
+ const columnType = options?.columnType ?? "text";
33
48
  return class extends Type {
34
49
  convertToDatabaseValue(value) {
35
50
  return value;
@@ -37,12 +52,47 @@ function idType(codec) {
37
52
  convertToJSValue(value) {
38
53
  return readIdColumn(codec, value);
39
54
  }
55
+ getColumnType() {
56
+ return columnType;
57
+ }
58
+ };
59
+ }
60
+ /**
61
+ * Factory that returns a MikroORM `Type` subclass for a **nullable** `Id<Brand>` column.
62
+ *
63
+ * Behaves like {@link idType} but `convertToJSValue` returns `null` for `null` /
64
+ * `undefined` database values and `convertToDatabaseValue` passes `null` through
65
+ * unchanged. Use for optional foreign keys.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { Property } from "@mikro-orm/core";
70
+ * import { nullableIdType } from "@smonn/ids/mikro-orm";
71
+ * import { createTimestampId } from "@smonn/ids";
72
+ * import type { Id } from "@smonn/ids";
73
+ *
74
+ * const usr = createTimestampId("usr");
75
+ *
76
+ * class Post {
77
+ * @Property({ type: nullableIdType(usr), nullable: true })
78
+ * authorId!: Id<"usr"> | null;
79
+ * }
80
+ * ```
81
+ */
82
+ function nullableIdType(codec) {
83
+ return class extends Type {
84
+ convertToDatabaseValue(value) {
85
+ return value;
86
+ }
87
+ convertToJSValue(value) {
88
+ return readIdColumnNullable(codec, value);
89
+ }
40
90
  getColumnType() {
41
91
  return "text";
42
92
  }
43
93
  };
44
94
  }
45
95
  //#endregion
46
- export { IdsError, idType, isIdsError };
96
+ export { IdsError, idType, isIdsError, nullableIdType };
47
97
 
48
98
  //# sourceMappingURL=mikro-orm.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mikro-orm.mjs","names":[],"sources":["../src/adapters/mikro-orm.ts"],"sourcesContent":["import { Type } from \"@mikro-orm/core\";\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 * Factory that returns a MikroORM `Type` subclass bound to a codec.\n *\n * **Write path** (`convertToDatabaseValue`): passes the `Id<Brand>` through\n * unchanged — it is already the canonical string form.\n *\n * **Read path** (`convertToJSValue`): normalises the raw DB value via\n * `codec.safeParse()`. Throws `IdsError(\"invalid_id\")` if the stored value\n * does not parse as a valid `Id<Brand>`.\n *\n * **Column type** (`getColumnType`): returns `\"text\"`.\n *\n * @example\n * ```ts\n * import { PrimaryKey } from \"@mikro-orm/core\";\n * import { idType } from \"@smonn/ids/mikro-orm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * class User {\n * @PrimaryKey({ type: idType(usr) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idType<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): new () => Type<Id<Brand>, string> {\n return class extends Type<Id<Brand>, string> {\n override convertToDatabaseValue(value: Id<Brand>): string {\n return value;\n }\n override convertToJSValue(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n }\n override getColumnType(): string {\n return \"text\";\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,OACd,OACmC;CACnC,OAAO,cAAc,KAAwB;EAC3C,uBAAgC,OAA0B;GACxD,OAAO;EACT;EACA,iBAA0B,OAA0B;GAClD,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,gBAAiC;GAC/B,OAAO;EACT;CACF;AACF"}
1
+ {"version":3,"file":"mikro-orm.mjs","names":[],"sources":["../src/adapters/mikro-orm.ts"],"sourcesContent":["import { Type } from \"@mikro-orm/core\";\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 * Factory that returns a MikroORM `Type` subclass bound to a codec.\n *\n * **Write path** (`convertToDatabaseValue`): passes the `Id<Brand>` through\n * unchanged — it is already the canonical string form.\n *\n * **Read path** (`convertToJSValue`): normalises the raw DB value via\n * `codec.safeParse()`. Throws `IdsError(\"invalid_id\")` if the stored value\n * does not parse as a valid `Id<Brand>`.\n *\n * **Column type** (`getColumnType`): returns `\"text\"` by default, or the\n * `options.columnType` override when provided.\n *\n * @param codec - The brand-scoped codec used to parse values read from the database.\n * @param options - Optional column configuration.\n * @param options.columnType - SQL column type to use (default: `\"text\"`). Pass\n * `\"varchar(30)\"` or `\"char(26)\"` to match an existing DDL or index strategy.\n * The value is passed through verbatim — no validation is performed.\n *\n * @example\n * ```ts\n * import { PrimaryKey } from \"@mikro-orm/core\";\n * import { idType } from \"@smonn/ids/mikro-orm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // default: text column\n * class User {\n * @PrimaryKey({ type: idType(usr) })\n * id!: Id<\"usr\">;\n * }\n *\n * // explicit varchar column\n * class Org {\n * @PrimaryKey({ type: idType(usr, { columnType: \"varchar(30)\" }) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idType<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n options?: { columnType?: string },\n): new () => Type<Id<Brand>, string> {\n const columnType = options?.columnType ?? \"text\";\n return class extends Type<Id<Brand>, string> {\n override convertToDatabaseValue(value: Id<Brand>): string {\n return value;\n }\n override convertToJSValue(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n }\n override getColumnType(): string {\n return columnType;\n }\n };\n}\n\n/**\n * Factory that returns a MikroORM `Type` subclass for a **nullable** `Id<Brand>` column.\n *\n * Behaves like {@link idType} but `convertToJSValue` returns `null` for `null` /\n * `undefined` database values and `convertToDatabaseValue` passes `null` through\n * unchanged. Use for optional foreign keys.\n *\n * @example\n * ```ts\n * import { Property } from \"@mikro-orm/core\";\n * import { nullableIdType } from \"@smonn/ids/mikro-orm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * class Post {\n * @Property({ type: nullableIdType(usr), nullable: true })\n * authorId!: Id<\"usr\"> | null;\n * }\n * ```\n */\nexport function nullableIdType<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): new () => Type<Id<Brand> | null, string | null> {\n return class extends Type<Id<Brand> | null, string | null> {\n override convertToDatabaseValue(value: Id<Brand> | null): string | null {\n return value;\n }\n override convertToJSValue(value: string | null): Id<Brand> | null {\n return readIdColumnNullable(codec, value);\n }\n override getColumnType(): string {\n return \"text\";\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,SAAgB,OACd,OACA,SACmC;CACnC,MAAM,aAAa,SAAS,cAAc;CAC1C,OAAO,cAAc,KAAwB;EAC3C,uBAAgC,OAA0B;GACxD,OAAO;EACT;EACA,iBAA0B,OAA0B;GAClD,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,gBAAiC;GAC/B,OAAO;EACT;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,eACd,OACiD;CACjD,OAAO,cAAc,KAAsC;EACzD,uBAAgC,OAAwC;GACtE,OAAO;EACT;EACA,iBAA0B,OAAwC;GAChE,OAAO,qBAAqB,OAAO,KAAK;EAC1C;EACA,gBAAiC;GAC/B,OAAO;EACT;CACF;AACF"}
package/dist/nestjs.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
1
+ import { r as resolveIdParamFailure } from "./adapter-types-CjzFNDcJ.mjs";
2
2
  import { BadRequestException, HttpException, Injectable, NotFoundException } from "@nestjs/common";
3
3
  //#region src/adapters/nestjs.ts
4
4
  /**
@@ -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