@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 +3 -2
- package/dist/graphql.d.mts +31 -0
- package/dist/graphql.d.mts.map +1 -0
- package/dist/graphql.mjs +42 -0
- package/dist/graphql.mjs.map +1 -0
- package/dist/mikro-orm.d.mts +37 -0
- package/dist/mikro-orm.d.mts.map +1 -0
- package/dist/mikro-orm.mjs +48 -0
- package/dist/mikro-orm.mjs.map +1 -0
- package/dist/nestjs.d.mts +70 -0
- package/dist/nestjs.d.mts.map +1 -0
- package/dist/nestjs.mjs +61 -0
- package/dist/nestjs.mjs.map +1 -0
- package/dist/typeorm.d.mts +41 -0
- package/dist/typeorm.d.mts.map +1 -0
- package/dist/typeorm.mjs +49 -0
- package/dist/typeorm.mjs.map +1 -0
- package/package.json +28 -2
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/)
|
|
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"}
|
package/dist/graphql.mjs
ADDED
|
@@ -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"}
|
package/dist/nestjs.mjs
ADDED
|
@@ -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"}
|
package/dist/typeorm.mjs
ADDED
|
@@ -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.
|
|
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": {
|