@smonn/ids 0.11.0 → 0.12.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 CHANGED
@@ -69,8 +69,9 @@ Try them all live in the [playground](https://ids.smonn.se/playground/).
69
69
  Framework and ORM adapters ship as optional subpath exports (each requires its
70
70
  own peer dependency):
71
71
 
72
- - **HTTP route params:** [Hono](https://ids.smonn.se/adapters/hono/), [Express](https://ids.smonn.se/adapters/express/), [Fastify](https://ids.smonn.se/adapters/fastify/) — `idParam` middleware
73
- - **ORM columns:** [Drizzle](https://ids.smonn.se/adapters/drizzle/), [Kysely](https://ids.smonn.se/adapters/kysely/), [Prisma](https://ids.smonn.se/adapters/prisma/)
72
+ - **HTTP route params:** [Hono](https://ids.smonn.se/adapters/hono/), [Express](https://ids.smonn.se/adapters/express/), [Fastify](https://ids.smonn.se/adapters/fastify/) — `idParam` middleware; [NestJS](https://ids.smonn.se/adapters/nestjs/) — `ParseIdPipe`
73
+ - **ORM columns:** [Drizzle](https://ids.smonn.se/adapters/drizzle/) — `idColumn`, [Kysely](https://ids.smonn.se/adapters/kysely/) — `idColumn`, [MikroORM](https://ids.smonn.se/adapters/mikro-orm/) — `idType`, [Prisma](https://ids.smonn.se/adapters/prisma/) — `idField`, [TypeORM](https://ids.smonn.se/adapters/typeorm/) — `idTransformer`
74
+ - **GraphQL:** [GraphQL](https://ids.smonn.se/adapters/graphql/) — `idScalar` custom scalar
74
75
  - **CLI:** brand-agnostic `inspect` / `generate` / `keygen` — `npx @smonn/ids --help` ([docs](https://ids.smonn.se/cli/))
75
76
 
76
77
  Every codec also implements [Standard Schema v1](https://standardschema.dev/), so
@@ -0,0 +1,31 @@
1
+ import { t as Id } from "./types-g7CiQDyE.mjs";
2
+ import { t as IdCodec } from "./adapter-types-CdYJM6Sf.mjs";
3
+ import { GraphQLScalarType } from "graphql";
4
+
5
+ //#region src/adapters/graphql.d.ts
6
+ /**
7
+ * Builds a `GraphQLScalarType` for the given codec and brand.
8
+ *
9
+ * - `serialize` — identity pass-through; an `Id<Brand>` is already the canonical wire string.
10
+ * - `parseValue` — validates variables via `codec.safeParse`; throws `GraphQLError` on failure.
11
+ * - `parseLiteral` — validates inline `Kind.STRING` literals; throws `GraphQLError` for any
12
+ * other AST kind or on a failed `safeParse`.
13
+ *
14
+ * `graphql` must be installed as a peer dependency.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { idScalar } from "@smonn/ids/graphql";
19
+ * import { createTimestampId } from "@smonn/ids";
20
+ *
21
+ * const usr = createTimestampId("usr");
22
+ * const UserIdScalar = idScalar(usr, { name: "UserId", description: "A branded user ID." });
23
+ * ```
24
+ */
25
+ declare function idScalar<Brand extends string>(codec: IdCodec<Brand>, config: {
26
+ name: string;
27
+ description?: string;
28
+ }): GraphQLScalarType<Id<Brand>, string>;
29
+ //#endregion
30
+ export { idScalar };
31
+ //# sourceMappingURL=graphql.d.mts.map
@@ -0,0 +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"}
@@ -0,0 +1,42 @@
1
+ import { GraphQLError, GraphQLScalarType, Kind } from "graphql";
2
+ //#region src/adapters/graphql.ts
3
+ /**
4
+ * Builds a `GraphQLScalarType` for the given codec and brand.
5
+ *
6
+ * - `serialize` — identity pass-through; an `Id<Brand>` is already the canonical wire string.
7
+ * - `parseValue` — validates variables via `codec.safeParse`; throws `GraphQLError` on failure.
8
+ * - `parseLiteral` — validates inline `Kind.STRING` literals; throws `GraphQLError` for any
9
+ * other AST kind or on a failed `safeParse`.
10
+ *
11
+ * `graphql` must be installed as a peer dependency.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { idScalar } from "@smonn/ids/graphql";
16
+ * import { createTimestampId } from "@smonn/ids";
17
+ *
18
+ * const usr = createTimestampId("usr");
19
+ * const UserIdScalar = idScalar(usr, { name: "UserId", description: "A branded user ID." });
20
+ * ```
21
+ */
22
+ function idScalar(codec, config) {
23
+ const parse = (value) => {
24
+ const result = codec.safeParse(value);
25
+ if (!result.ok) throw new GraphQLError(`invalid ${config.name}: ${result.error}`);
26
+ return result.id;
27
+ };
28
+ return new GraphQLScalarType({
29
+ name: config.name,
30
+ description: config.description,
31
+ serialize: (value) => value,
32
+ parseValue: parse,
33
+ parseLiteral: (ast) => {
34
+ if (ast.kind !== Kind.STRING) throw new GraphQLError(`${config.name} must be a string literal, got ${ast.kind}`);
35
+ return parse(ast.value);
36
+ }
37
+ });
38
+ }
39
+ //#endregion
40
+ export { idScalar };
41
+
42
+ //# sourceMappingURL=graphql.mjs.map
@@ -0,0 +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` — identity pass-through; an `Id<Brand>` is already the canonical wire string.\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: (value) => value as string,\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,YAAY,UAAU;EACtB,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"}
@@ -0,0 +1,37 @@
1
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
+ import { t as Id } from "./types-g7CiQDyE.mjs";
3
+ import { n as IdColumnCodec } from "./adapter-types-CdYJM6Sf.mjs";
4
+ import { Type } from "@mikro-orm/core";
5
+
6
+ //#region src/adapters/mikro-orm.d.ts
7
+ /**
8
+ * Factory that returns a MikroORM `Type` subclass bound to a codec.
9
+ *
10
+ * **Write path** (`convertToDatabaseValue`): passes the `Id<Brand>` through
11
+ * unchanged — it is already the canonical string form.
12
+ *
13
+ * **Read path** (`convertToJSValue`): normalises the raw DB value via
14
+ * `codec.safeParse()`. Throws `IdsError("invalid_id")` if the stored value
15
+ * does not parse as a valid `Id<Brand>`.
16
+ *
17
+ * **Column type** (`getColumnType`): returns `"text"`.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { PrimaryKey } from "@mikro-orm/core";
22
+ * import { idType } from "@smonn/ids/mikro-orm";
23
+ * import { createTimestampId } from "@smonn/ids";
24
+ * import type { Id } from "@smonn/ids";
25
+ *
26
+ * const usr = createTimestampId("usr");
27
+ *
28
+ * class User {
29
+ * @PrimaryKey({ type: idType(usr) })
30
+ * id!: Id<"usr">;
31
+ * }
32
+ * ```
33
+ */
34
+ declare function idType<Brand extends string>(codec: IdColumnCodec<Brand>): new () => Type<Id<Brand>, string>;
35
+ //#endregion
36
+ export { type IdColumnCodec, IdsError, type IdsErrorCode, idType, isIdsError };
37
+ //# sourceMappingURL=mikro-orm.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mikro-orm.d.mts","names":[],"sources":["../src/adapters/mikro-orm.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuCqB;;;;;;;;;;iBAFL,MAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,cACV,IAAA,CAAK,EAAA,CAAG,KAAA"}
@@ -0,0 +1,48 @@
1
+ import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
+ import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
3
+ import { Type } from "@mikro-orm/core";
4
+ //#region src/adapters/mikro-orm.ts
5
+ /**
6
+ * Factory that returns a MikroORM `Type` subclass bound to a codec.
7
+ *
8
+ * **Write path** (`convertToDatabaseValue`): passes the `Id<Brand>` through
9
+ * unchanged — it is already the canonical string form.
10
+ *
11
+ * **Read path** (`convertToJSValue`): normalises the raw DB value via
12
+ * `codec.safeParse()`. Throws `IdsError("invalid_id")` if the stored value
13
+ * does not parse as a valid `Id<Brand>`.
14
+ *
15
+ * **Column type** (`getColumnType`): returns `"text"`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { PrimaryKey } from "@mikro-orm/core";
20
+ * import { idType } from "@smonn/ids/mikro-orm";
21
+ * import { createTimestampId } from "@smonn/ids";
22
+ * import type { Id } from "@smonn/ids";
23
+ *
24
+ * const usr = createTimestampId("usr");
25
+ *
26
+ * class User {
27
+ * @PrimaryKey({ type: idType(usr) })
28
+ * id!: Id<"usr">;
29
+ * }
30
+ * ```
31
+ */
32
+ function idType(codec) {
33
+ return class extends Type {
34
+ convertToDatabaseValue(value) {
35
+ return value;
36
+ }
37
+ convertToJSValue(value) {
38
+ return readIdColumn(codec, value);
39
+ }
40
+ getColumnType() {
41
+ return "text";
42
+ }
43
+ };
44
+ }
45
+ //#endregion
46
+ export { IdsError, idType, isIdsError };
47
+
48
+ //# sourceMappingURL=mikro-orm.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mikro-orm.mjs","names":[],"sources":["../src/adapters/mikro-orm.ts"],"sourcesContent":["import { Type } from \"@mikro-orm/core\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\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 };\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,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"}
@@ -0,0 +1,70 @@
1
+ import { t as Id } from "./types-g7CiQDyE.mjs";
2
+ import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CdYJM6Sf.mjs";
3
+ import { ArgumentMetadata, PipeTransform } from "@nestjs/common";
4
+
5
+ //#region src/adapters/nestjs.d.ts
6
+ /**
7
+ * Options for `ParseIdPipe`. All fields are optional.
8
+ *
9
+ * **`onError` constraint:** NestJS `transform()` receives only `value` and `ArgumentMetadata`
10
+ * — there is no HTTP context object. The `onError` hook must throw (or re-throw); it cannot
11
+ * write a response inline the way Hono/Express hooks can.
12
+ */
13
+ type IdParamOptions = {
14
+ /**
15
+ * Called instead of throwing when provided. The hook **must** throw or re-throw — it cannot
16
+ * return a response because `PipeTransform.transform` has no HTTP context.
17
+ */
18
+ onError?: (failure: IdParamFailure) => never;
19
+ /**
20
+ * Remap the default HTTP status for a failure reason without a full handler.
21
+ * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.
22
+ */
23
+ status?: {
24
+ brand_mismatch?: number;
25
+ malformed?: number;
26
+ };
27
+ };
28
+ /**
29
+ * NestJS pipe that validates an untrusted route param against a codec via `safeParse`.
30
+ *
31
+ * Marked `@Injectable()` via `Injectable()(ParseIdPipe)` at module load time, making it
32
+ * available for NestJS DI.
33
+ *
34
+ * **Default (no options):** throws `NotFoundException` (404) for brand mismatches and
35
+ * `BadRequestException` (400) for malformed IDs.
36
+ *
37
+ * **`options.status`:** remaps the default HTTP status for a reason; when the resolved status
38
+ * differs from the default, the pipe throws `HttpException(reason, status)`.
39
+ *
40
+ * **`options.onError`:** escape hatch for custom error handling. The hook must throw — it
41
+ * cannot return a response because `PipeTransform.transform` has no HTTP context.
42
+ *
43
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
44
+ * - **Malformed or missing ID → `reason: "malformed"`, default 400**
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { ParseIdPipe } from "@smonn/ids/nestjs";
49
+ * import { createTimestampId } from "@smonn/ids";
50
+ *
51
+ * const usr = createTimestampId("usr");
52
+ *
53
+ * @Controller("users")
54
+ * class UsersController {
55
+ * @Get(":id")
56
+ * findOne(@Param("id", new ParseIdPipe(usr)) id: Id<"usr">) {
57
+ * return { id }; // Id<"usr">, canonical
58
+ * }
59
+ * }
60
+ * ```
61
+ */
62
+ declare class ParseIdPipe<Brand extends string> implements PipeTransform<unknown, Id<Brand>> {
63
+ private readonly codec;
64
+ private readonly options;
65
+ constructor(codec: IdCodec<Brand>, options?: IdParamOptions);
66
+ transform(value: unknown, _metadata: ArgumentMetadata): Id<Brand>;
67
+ }
68
+ //#endregion
69
+ export { type IdParamFailure, IdParamOptions, ParseIdPipe };
70
+ //# sourceMappingURL=nestjs.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nestjs.d.mts","names":[],"sources":["../src/adapters/nestjs.ts"],"mappings":";;;;;;AAcA;;;;;;KAAY,cAAA;;;;;EAKV,OAAA,IAAW,OAAA,EAAS,cAAA;EA0CtB;;;;EArCE,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;AA8CuB;;;;;;;;;;cAThD,WAAA,kCAA6C,aAAA,UAAuB,EAAA,CAAG,KAAA;EAAA,iBACjE,KAAA;EAAA,iBACA,OAAA;EAEjB,WAAA,CAAY,KAAA,EAAO,OAAA,CAAQ,KAAA,GAAQ,OAAA,GAAU,cAAA;EAK7C,SAAA,CAAU,KAAA,WAAgB,SAAA,EAAW,gBAAA,GAAmB,EAAA,CAAG,KAAA;AAAA"}
@@ -0,0 +1,61 @@
1
+ import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
2
+ import { BadRequestException, HttpException, Injectable, NotFoundException } from "@nestjs/common";
3
+ //#region src/adapters/nestjs.ts
4
+ /**
5
+ * NestJS pipe that validates an untrusted route param against a codec via `safeParse`.
6
+ *
7
+ * Marked `@Injectable()` via `Injectable()(ParseIdPipe)` at module load time, making it
8
+ * available for NestJS DI.
9
+ *
10
+ * **Default (no options):** throws `NotFoundException` (404) for brand mismatches and
11
+ * `BadRequestException` (400) for malformed IDs.
12
+ *
13
+ * **`options.status`:** remaps the default HTTP status for a reason; when the resolved status
14
+ * differs from the default, the pipe throws `HttpException(reason, status)`.
15
+ *
16
+ * **`options.onError`:** escape hatch for custom error handling. The hook must throw — it
17
+ * cannot return a response because `PipeTransform.transform` has no HTTP context.
18
+ *
19
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
20
+ * - **Malformed or missing ID → `reason: "malformed"`, default 400**
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { ParseIdPipe } from "@smonn/ids/nestjs";
25
+ * import { createTimestampId } from "@smonn/ids";
26
+ *
27
+ * const usr = createTimestampId("usr");
28
+ *
29
+ * @Controller("users")
30
+ * class UsersController {
31
+ * @Get(":id")
32
+ * findOne(@Param("id", new ParseIdPipe(usr)) id: Id<"usr">) {
33
+ * return { id }; // Id<"usr">, canonical
34
+ * }
35
+ * }
36
+ * ```
37
+ */
38
+ var ParseIdPipe = class {
39
+ codec;
40
+ options;
41
+ constructor(codec, options) {
42
+ this.codec = codec;
43
+ this.options = options;
44
+ }
45
+ transform(value, _metadata) {
46
+ const result = this.codec.safeParse(value);
47
+ if (!result.ok) {
48
+ const failure = resolveIdParamFailure(result.error, this.options);
49
+ if (this.options?.onError) this.options.onError(failure);
50
+ if (failure.reason === "brand_mismatch" && failure.status === 404) throw new NotFoundException();
51
+ if (failure.reason === "malformed" && failure.status === 400) throw new BadRequestException();
52
+ throw new HttpException(failure.reason, failure.status);
53
+ }
54
+ return result.id;
55
+ }
56
+ };
57
+ Injectable()(ParseIdPipe);
58
+ //#endregion
59
+ export { ParseIdPipe };
60
+
61
+ //# sourceMappingURL=nestjs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nestjs.mjs","names":[],"sources":["../src/adapters/nestjs.ts"],"sourcesContent":["import { BadRequestException, HttpException, Injectable, NotFoundException } from \"@nestjs/common\";\nimport type { ArgumentMetadata, PipeTransform } from \"@nestjs/common\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Options for `ParseIdPipe`. All fields are optional.\n *\n * **`onError` constraint:** NestJS `transform()` receives only `value` and `ArgumentMetadata`\n * — there is no HTTP context object. The `onError` hook must throw (or re-throw); it cannot\n * write a response inline the way Hono/Express hooks can.\n */\nexport type IdParamOptions = {\n /**\n * Called instead of throwing when provided. The hook **must** throw or re-throw — it cannot\n * return a response because `PipeTransform.transform` has no HTTP context.\n */\n onError?: (failure: IdParamFailure) => never;\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?: number; malformed?: number };\n};\n\n/**\n * NestJS pipe that validates an untrusted route param against a codec via `safeParse`.\n *\n * Marked `@Injectable()` via `Injectable()(ParseIdPipe)` at module load time, making it\n * available for NestJS DI.\n *\n * **Default (no options):** throws `NotFoundException` (404) for brand mismatches and\n * `BadRequestException` (400) for malformed IDs.\n *\n * **`options.status`:** remaps the default HTTP status for a reason; when the resolved status\n * differs from the default, the pipe throws `HttpException(reason, status)`.\n *\n * **`options.onError`:** escape hatch for custom error handling. The hook must throw — it\n * cannot return a response because `PipeTransform.transform` has no HTTP context.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing ID → `reason: \"malformed\"`, default 400**\n *\n * @example\n * ```ts\n * import { ParseIdPipe } from \"@smonn/ids/nestjs\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Controller(\"users\")\n * class UsersController {\n * @Get(\":id\")\n * findOne(@Param(\"id\", new ParseIdPipe(usr)) id: Id<\"usr\">) {\n * return { id }; // Id<\"usr\">, canonical\n * }\n * }\n * ```\n */\nexport class ParseIdPipe<Brand extends string> implements PipeTransform<unknown, Id<Brand>> {\n private readonly codec: IdCodec<Brand>;\n private readonly options: IdParamOptions | undefined;\n\n constructor(codec: IdCodec<Brand>, options?: IdParamOptions) {\n this.codec = codec;\n this.options = options;\n }\n\n transform(value: unknown, _metadata: ArgumentMetadata): Id<Brand> {\n const result = this.codec.safeParse(value);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, this.options);\n if (this.options?.onError) {\n this.options.onError(failure);\n }\n if (failure.reason === \"brand_mismatch\" && failure.status === 404) {\n throw new NotFoundException();\n }\n if (failure.reason === \"malformed\" && failure.status === 400) {\n throw new BadRequestException();\n }\n throw new HttpException(failure.reason, failure.status);\n }\n return result.id;\n }\n}\n\n// Apply @Injectable() metadata so ParseIdPipe participates in NestJS DI when provided as a class.\n// Using a call instead of the @Injectable() decorator syntax to remain compatible with\n// TypeScript projects that do not enable experimentalDecorators.\nInjectable()(ParseIdPipe as unknown as new (...args: unknown[]) => unknown);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,IAAa,cAAb,MAA4F;CAC1F;CACA;CAEA,YAAY,OAAuB,SAA0B;EAC3D,KAAK,QAAQ;EACb,KAAK,UAAU;CACjB;CAEA,UAAU,OAAgB,WAAwC;EAChE,MAAM,SAAS,KAAK,MAAM,UAAU,KAAK;EACzC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,KAAK,OAAO;GAChE,IAAI,KAAK,SAAS,SAChB,KAAK,QAAQ,QAAQ,OAAO;GAE9B,IAAI,QAAQ,WAAW,oBAAoB,QAAQ,WAAW,KAC5D,MAAM,IAAI,kBAAkB;GAE9B,IAAI,QAAQ,WAAW,eAAe,QAAQ,WAAW,KACvD,MAAM,IAAI,oBAAoB;GAEhC,MAAM,IAAI,cAAc,QAAQ,QAAQ,QAAQ,MAAM;EACxD;EACA,OAAO,OAAO;CAChB;AACF;AAKA,WAAW,CAAC,CAAC,WAA6D"}
@@ -0,0 +1,41 @@
1
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
+ import { n as IdColumnCodec } from "./adapter-types-CdYJM6Sf.mjs";
3
+ import { ValueTransformer } from "typeorm";
4
+
5
+ //#region src/adapters/typeorm.d.ts
6
+ /**
7
+ * TypeORM column transformer for `Id<Brand>`.
8
+ *
9
+ * Returns a `ValueTransformer` object suitable for use in a TypeORM `@Column`
10
+ * decorator's `transformer` option.
11
+ *
12
+ * **Write path (`to`):** passes the `Id<Brand>` directly to the database — it is
13
+ * already the canonical string form.
14
+ *
15
+ * **Read path (`from`):** normalises the raw database value via `codec.safeParse()`.
16
+ * Throws `IdsError` with code `"invalid_id"` if the value does not parse as a valid
17
+ * `Id<Brand>`.
18
+ *
19
+ * **TypeORM branding caveat:** TypeORM cannot brand a generated entity field type at
20
+ * the schema level. Annotate the entity field explicitly: `id!: Id<"usr">`.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { idTransformer } from "@smonn/ids/typeorm";
25
+ * import { createTimestampId } from "@smonn/ids";
26
+ * import type { Id } from "@smonn/ids";
27
+ * import { Column, Entity } from "typeorm";
28
+ *
29
+ * const usr = createTimestampId("usr");
30
+ *
31
+ * @Entity()
32
+ * class User {
33
+ * @Column({ type: "text", transformer: idTransformer(usr) })
34
+ * id!: Id<"usr">;
35
+ * }
36
+ * ```
37
+ */
38
+ declare function idTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer;
39
+ //#endregion
40
+ export { type IdColumnCodec, IdsError, type IdsErrorCode, idTransformer, isIdsError };
41
+ //# sourceMappingURL=typeorm.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typeorm.d.mts","names":[],"sources":["../src/adapters/typeorm.ts"],"mappings":";;;;;;;;;;;;;;;;;;AA0CkF;;;;;;;;;;;;;;;;;;;iBAAlE,aAAA,uBAAoC,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,gBAAA"}
@@ -0,0 +1,49 @@
1
+ import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
+ import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
3
+ //#region src/adapters/typeorm.ts
4
+ /**
5
+ * TypeORM column transformer for `Id<Brand>`.
6
+ *
7
+ * Returns a `ValueTransformer` object suitable for use in a TypeORM `@Column`
8
+ * decorator's `transformer` option.
9
+ *
10
+ * **Write path (`to`):** passes the `Id<Brand>` directly to the database — it is
11
+ * already the canonical string form.
12
+ *
13
+ * **Read path (`from`):** normalises the raw database value via `codec.safeParse()`.
14
+ * Throws `IdsError` with code `"invalid_id"` if the value does not parse as a valid
15
+ * `Id<Brand>`.
16
+ *
17
+ * **TypeORM branding caveat:** TypeORM cannot brand a generated entity field type at
18
+ * the schema level. Annotate the entity field explicitly: `id!: Id<"usr">`.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { idTransformer } from "@smonn/ids/typeorm";
23
+ * import { createTimestampId } from "@smonn/ids";
24
+ * import type { Id } from "@smonn/ids";
25
+ * import { Column, Entity } from "typeorm";
26
+ *
27
+ * const usr = createTimestampId("usr");
28
+ *
29
+ * @Entity()
30
+ * class User {
31
+ * @Column({ type: "text", transformer: idTransformer(usr) })
32
+ * id!: Id<"usr">;
33
+ * }
34
+ * ```
35
+ */
36
+ function idTransformer(codec) {
37
+ return {
38
+ to(value) {
39
+ return value;
40
+ },
41
+ from(value) {
42
+ return readIdColumn(codec, value);
43
+ }
44
+ };
45
+ }
46
+ //#endregion
47
+ export { IdsError, idTransformer, isIdsError };
48
+
49
+ //# sourceMappingURL=typeorm.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typeorm.mjs","names":[],"sources":["../src/adapters/typeorm.ts"],"sourcesContent":["import type { ValueTransformer } from \"typeorm\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\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 };\n\nexport type { IdColumnCodec };\n\n/**\n * TypeORM column transformer for `Id<Brand>`.\n *\n * Returns a `ValueTransformer` object suitable for use in a TypeORM `@Column`\n * decorator's `transformer` option.\n *\n * **Write path (`to`):** passes the `Id<Brand>` directly to the database — it is\n * already the canonical string form.\n *\n * **Read path (`from`):** normalises the raw database value via `codec.safeParse()`.\n * Throws `IdsError` with code `\"invalid_id\"` if the value does not parse as a valid\n * `Id<Brand>`.\n *\n * **TypeORM branding caveat:** TypeORM cannot brand a generated entity field type at\n * the schema level. Annotate the entity field explicitly: `id!: Id<\"usr\">`.\n *\n * @example\n * ```ts\n * import { idTransformer } from \"@smonn/ids/typeorm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n * import { Column, Entity } from \"typeorm\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Entity()\n * class User {\n * @Column({ type: \"text\", transformer: idTransformer(usr) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer {\n return {\n to(value: Id<Brand>): string {\n return value;\n },\n from(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,cAAoC,OAA+C;CACjG,OAAO;EACL,GAAG,OAA0B;GAC3B,OAAO;EACT;EACA,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smonn/ids",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "license": "MIT",
5
5
  "author": "Simon Ingeson (https://github.com/smonn)",
6
6
  "repository": {
@@ -24,13 +24,19 @@
24
24
  "./drizzle": "./dist/drizzle.mjs",
25
25
  "./hono": "./dist/hono.mjs",
26
26
  "./kysely": "./dist/kysely.mjs",
27
+ "./mikro-orm": "./dist/mikro-orm.mjs",
27
28
  "./prisma": "./dist/prisma.mjs",
28
29
  "./express": "./dist/express.mjs",
29
30
  "./fastify": "./dist/fastify.mjs",
31
+ "./typeorm": "./dist/typeorm.mjs",
32
+ "./graphql": "./dist/graphql.mjs",
33
+ "./nestjs": "./dist/nestjs.mjs",
30
34
  "./package.json": "./package.json"
31
35
  },
32
36
  "devDependencies": {
33
37
  "@changesets/cli": "2.31.0",
38
+ "@mikro-orm/core": "^7.1.4",
39
+ "@nestjs/common": "^11.1.27",
34
40
  "@prisma/client": ">=5.0.0",
35
41
  "@types/express": "^5.0.6",
36
42
  "@types/node": "24.13.2",
@@ -39,25 +45,36 @@
39
45
  "drizzle-orm": "^0.45.2",
40
46
  "express": "^5.2.1",
41
47
  "fastify": "^5.8.5",
48
+ "graphql": "^17.0.1",
42
49
  "hono": "^4.12.26",
43
50
  "knip": "6.18.0",
44
51
  "kysely": "^0.29.2",
45
52
  "mitata": "1.0.34",
46
53
  "oxfmt": "0.55.0",
47
54
  "oxlint": "1.70.0",
55
+ "reflect-metadata": "^0.2.2",
48
56
  "tsdown": "0.22.3",
57
+ "tslib": "^2.8.1",
58
+ "typeorm": "^1.0.0",
49
59
  "typescript": "6.0.3",
50
60
  "vitest": "4.1.9"
51
61
  },
52
62
  "peerDependencies": {
63
+ "@mikro-orm/core": ">=6.0.0",
64
+ "@nestjs/common": ">=10.0.0",
53
65
  "@prisma/client": ">=5.0.0",
54
66
  "drizzle-orm": ">=0.30.0",
55
67
  "express": ">=4.0.0",
56
68
  "fastify": ">=4.0.0",
69
+ "graphql": ">=16.0.0",
57
70
  "hono": ">=4.0.0",
58
- "kysely": ">=0.27.0"
71
+ "kysely": ">=0.27.0",
72
+ "typeorm": ">=0.3.0"
59
73
  },
60
74
  "peerDependenciesMeta": {
75
+ "@mikro-orm/core": {
76
+ "optional": true
77
+ },
61
78
  "drizzle-orm": {
62
79
  "optional": true
63
80
  },
@@ -67,6 +84,9 @@
67
84
  "fastify": {
68
85
  "optional": true
69
86
  },
87
+ "graphql": {
88
+ "optional": true
89
+ },
70
90
  "hono": {
71
91
  "optional": true
72
92
  },
@@ -75,6 +95,12 @@
75
95
  },
76
96
  "@prisma/client": {
77
97
  "optional": true
98
+ },
99
+ "typeorm": {
100
+ "optional": true
101
+ },
102
+ "@nestjs/common": {
103
+ "optional": true
78
104
  }
79
105
  },
80
106
  "engines": {