@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,5 +1,6 @@
1
1
  import { t as Id } from "./types-hGBnCpJj.mjs";
2
2
  import { t as IdCodec } from "./adapter-types-Bia_w9sg.mjs";
3
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-CifcKKOG.mjs";
3
4
  import { GraphQLScalarType } from "graphql";
4
5
 
5
6
  //#region src/adapters/graphql.d.ts
@@ -27,5 +28,5 @@ declare function idScalar<Brand extends string>(codec: IdCodec<Brand>, config: {
27
28
  description?: string;
28
29
  }): GraphQLScalarType<Id<Brand>, string>;
29
30
  //#endregion
30
- export { idScalar };
31
+ export { IdsError, type IdsErrorCode, idScalar, isIdsError };
31
32
  //# sourceMappingURL=graphql.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graphql.d.mts","names":[],"sources":["../src/adapters/graphql.ts"],"mappings":";;;;;;;AAwBA;;;;;;;;;;;;;;;;;iBAAgB,QAAA,uBACd,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,MAAA;EAAU,IAAA;EAAc,WAAA;AAAA,IACvB,iBAAA,CAAkB,EAAA,CAAG,KAAA"}
1
+ {"version":3,"file":"graphql.d.mts","names":[],"sources":["../src/adapters/graphql.ts"],"mappings":";;;;;;;AA2BA;;;;;;;;;;;;;;;;;;iBAAgB,QAAA,uBACd,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,MAAA;EAAU,IAAA;EAAc,WAAA;AAAA,IACvB,iBAAA,CAAkB,EAAA,CAAG,KAAA"}
package/dist/graphql.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
1
2
  import { GraphQLError, GraphQLScalarType, Kind } from "graphql";
2
3
  //#region src/adapters/graphql.ts
3
4
  /**
@@ -37,6 +38,6 @@ function idScalar(codec, config) {
37
38
  });
38
39
  }
39
40
  //#endregion
40
- export { idScalar };
41
+ export { IdsError, idScalar, isIdsError };
41
42
 
42
43
  //# sourceMappingURL=graphql.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"graphql.mjs","names":[],"sources":["../src/adapters/graphql.ts"],"sourcesContent":["import { GraphQLError, GraphQLScalarType, Kind } from \"graphql\";\nimport type { ValueNode } from \"graphql\";\nimport type { IdCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/**\n * Builds a `GraphQLScalarType` for the given codec and brand.\n *\n * - `serialize` — validates via `codec.safeParse`; throws `GraphQLError` on a non-conforming value.\n * - `parseValue` — validates variables via `codec.safeParse`; throws `GraphQLError` on failure.\n * - `parseLiteral` — validates inline `Kind.STRING` literals; throws `GraphQLError` for any\n * other AST kind or on a failed `safeParse`.\n *\n * `graphql` must be installed as a peer dependency.\n *\n * @example\n * ```ts\n * import { idScalar } from \"@smonn/ids/graphql\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const UserIdScalar = idScalar(usr, { name: \"UserId\", description: \"A branded user ID.\" });\n * ```\n */\nexport function idScalar<Brand extends string>(\n codec: IdCodec<Brand>,\n config: { name: string; description?: string },\n): GraphQLScalarType<Id<Brand>, string> {\n const parse = (value: unknown): Id<Brand> => {\n const result = codec.safeParse(value);\n if (!result.ok) {\n throw new GraphQLError(`invalid ${config.name}: ${result.error}`);\n }\n return result.id;\n };\n return new GraphQLScalarType<Id<Brand>, string>({\n name: config.name,\n description: config.description,\n serialize: parse,\n parseValue: parse,\n parseLiteral: (ast: ValueNode) => {\n if (ast.kind !== Kind.STRING) {\n throw new GraphQLError(`${config.name} must be a string literal, got ${ast.kind}`);\n }\n return parse(ast.value);\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,SACd,OACA,QACsC;CACtC,MAAM,SAAS,UAA8B;EAC3C,MAAM,SAAS,MAAM,UAAU,KAAK;EACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,aAAa,WAAW,OAAO,KAAK,IAAI,OAAO,OAAO;EAElE,OAAO,OAAO;CAChB;CACA,OAAO,IAAI,kBAAqC;EAC9C,MAAM,OAAO;EACb,aAAa,OAAO;EACpB,WAAW;EACX,YAAY;EACZ,eAAe,QAAmB;GAChC,IAAI,IAAI,SAAS,KAAK,QACpB,MAAM,IAAI,aAAa,GAAG,OAAO,KAAK,iCAAiC,IAAI,MAAM;GAEnF,OAAO,MAAM,IAAI,KAAK;EACxB;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"graphql.mjs","names":[],"sources":["../src/adapters/graphql.ts"],"sourcesContent":["import { GraphQLError, GraphQLScalarType, Kind } from \"graphql\";\nimport type { ValueNode } from \"graphql\";\nimport type { IdCodec } 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\n/**\n * Builds a `GraphQLScalarType` for the given codec and brand.\n *\n * - `serialize` — validates via `codec.safeParse`; throws `GraphQLError` on a non-conforming value.\n * - `parseValue` — validates variables via `codec.safeParse`; throws `GraphQLError` on failure.\n * - `parseLiteral` — validates inline `Kind.STRING` literals; throws `GraphQLError` for any\n * other AST kind or on a failed `safeParse`.\n *\n * `graphql` must be installed as a peer dependency.\n *\n * @example\n * ```ts\n * import { idScalar } from \"@smonn/ids/graphql\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const UserIdScalar = idScalar(usr, { name: \"UserId\", description: \"A branded user ID.\" });\n * ```\n */\nexport function idScalar<Brand extends string>(\n codec: IdCodec<Brand>,\n config: { name: string; description?: string },\n): GraphQLScalarType<Id<Brand>, string> {\n const parse = (value: unknown): Id<Brand> => {\n const result = codec.safeParse(value);\n if (!result.ok) {\n throw new GraphQLError(`invalid ${config.name}: ${result.error}`);\n }\n return result.id;\n };\n return new GraphQLScalarType<Id<Brand>, string>({\n name: config.name,\n description: config.description,\n serialize: parse,\n parseValue: parse,\n parseLiteral: (ast: ValueNode) => {\n if (ast.kind !== Kind.STRING) {\n throw new GraphQLError(`${config.name} must be a string literal, got ${ast.kind}`);\n }\n return parse(ast.value);\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,SACd,OACA,QACsC;CACtC,MAAM,SAAS,UAA8B;EAC3C,MAAM,SAAS,MAAM,UAAU,KAAK;EACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,aAAa,WAAW,OAAO,KAAK,IAAI,OAAO,OAAO;EAElE,OAAO,OAAO;CAChB;CACA,OAAO,IAAI,kBAAqC;EAC9C,MAAM,OAAO;EACb,aAAa,OAAO;EACpB,WAAW;EACX,YAAY;EACZ,eAAe,QAAmB;GAChC,IAAI,IAAI,SAAS,KAAK,QACpB,MAAM,IAAI,aAAa,GAAG,OAAO,KAAK,iCAAiC,IAAI,MAAM;GAEnF,OAAO,MAAM,IAAI,KAAK;EACxB;CACF,CAAC;AACH"}
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
  /**