@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.
- package/README.md +3 -3
- package/dist/{adapter-types-7wWdELSh.mjs → adapter-types-CjzFNDcJ.mjs} +7 -2
- package/dist/adapter-types-CjzFNDcJ.mjs.map +1 -0
- package/dist/cli.mjs +30 -22
- package/dist/cli.mjs.map +1 -1
- package/dist/drizzle.d.mts +87 -3
- package/dist/drizzle.d.mts.map +1 -1
- package/dist/drizzle.mjs +115 -5
- package/dist/drizzle.mjs.map +1 -1
- package/dist/express.d.mts +44 -2
- package/dist/express.d.mts.map +1 -1
- package/dist/express.mjs +60 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.d.mts +49 -2
- package/dist/fastify.d.mts.map +1 -1
- package/dist/fastify.mjs +61 -2
- package/dist/fastify.mjs.map +1 -1
- package/dist/graphql.d.mts +2 -1
- package/dist/graphql.d.mts.map +1 -1
- package/dist/graphql.mjs +2 -1
- package/dist/graphql.mjs.map +1 -1
- package/dist/hono.d.mts +44 -2
- package/dist/hono.d.mts.map +1 -1
- package/dist/hono.mjs +54 -2
- package/dist/hono.mjs.map +1 -1
- package/dist/kysely.d.mts +73 -2
- package/dist/kysely.d.mts.map +1 -1
- package/dist/kysely.mjs +84 -2
- package/dist/kysely.mjs.map +1 -1
- package/dist/mikro-orm.d.mts +42 -3
- package/dist/mikro-orm.d.mts.map +1 -1
- package/dist/mikro-orm.mjs +54 -4
- package/dist/mikro-orm.mjs.map +1 -1
- package/dist/nestjs.mjs +1 -1
- package/dist/{opaque-COAcIIY4.mjs → opaque-Dle3CmSE.mjs} +18 -10
- package/dist/opaque-Dle3CmSE.mjs.map +1 -0
- package/dist/opaque.d.mts +16 -10
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +1 -1
- package/dist/prisma.d.mts +112 -9
- package/dist/prisma.d.mts.map +1 -1
- package/dist/prisma.mjs +101 -3
- package/dist/prisma.mjs.map +1 -1
- package/dist/typeorm.d.mts +25 -1
- package/dist/typeorm.d.mts.map +1 -1
- package/dist/typeorm.mjs +35 -2
- package/dist/typeorm.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/adapter-types-7wWdELSh.mjs.map +0 -1
- package/dist/opaque-COAcIIY4.mjs.map +0 -1
package/dist/graphql.d.mts
CHANGED
|
@@ -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
|
package/dist/graphql.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphql.d.mts","names":[],"sources":["../src/adapters/graphql.ts"],"mappings":";;;;;;;
|
|
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
|
package/dist/graphql.mjs.map
CHANGED
|
@@ -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":"
|
|
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
|
package/dist/hono.d.mts.map
CHANGED
|
@@ -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 {
|
|
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
|
package/dist/kysely.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kysely.d.mts","names":[],"sources":["../src/adapters/kysely.ts"],"mappings":";;;;;;
|
|
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-
|
|
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
|
package/dist/kysely.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kysely.mjs","names":[],"sources":["../src/adapters/kysely.ts"],"sourcesContent":["import type {
|
|
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"}
|
package/dist/mikro-orm.d.mts
CHANGED
|
@@ -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
|
|
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
|
package/dist/mikro-orm.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mikro-orm.d.mts","names":[],"sources":["../src/adapters/mikro-orm.ts"],"mappings":";;;;;;
|
|
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"}
|
package/dist/mikro-orm.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-
|
|
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
|
package/dist/mikro-orm.mjs.map
CHANGED
|
@@ -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\"
|
|
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 {
|
|
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
|
/**
|