@smonn/ids 0.15.0 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +3 -3
  2. package/dist/{adapter-types-7wWdELSh.mjs → adapter-types-CjzFNDcJ.mjs} +7 -2
  3. package/dist/adapter-types-CjzFNDcJ.mjs.map +1 -0
  4. package/dist/cli.mjs +30 -22
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/drizzle.d.mts +87 -3
  7. package/dist/drizzle.d.mts.map +1 -1
  8. package/dist/drizzle.mjs +115 -5
  9. package/dist/drizzle.mjs.map +1 -1
  10. package/dist/express.d.mts +44 -2
  11. package/dist/express.d.mts.map +1 -1
  12. package/dist/express.mjs +60 -2
  13. package/dist/express.mjs.map +1 -1
  14. package/dist/fastify.d.mts +49 -2
  15. package/dist/fastify.d.mts.map +1 -1
  16. package/dist/fastify.mjs +61 -2
  17. package/dist/fastify.mjs.map +1 -1
  18. package/dist/hono.d.mts +44 -2
  19. package/dist/hono.d.mts.map +1 -1
  20. package/dist/hono.mjs +54 -2
  21. package/dist/hono.mjs.map +1 -1
  22. package/dist/kysely.d.mts +73 -2
  23. package/dist/kysely.d.mts.map +1 -1
  24. package/dist/kysely.mjs +84 -2
  25. package/dist/kysely.mjs.map +1 -1
  26. package/dist/mikro-orm.d.mts +42 -3
  27. package/dist/mikro-orm.d.mts.map +1 -1
  28. package/dist/mikro-orm.mjs +54 -4
  29. package/dist/mikro-orm.mjs.map +1 -1
  30. package/dist/nestjs.mjs +1 -1
  31. package/dist/{opaque-COAcIIY4.mjs → opaque-Dle3CmSE.mjs} +18 -10
  32. package/dist/opaque-Dle3CmSE.mjs.map +1 -0
  33. package/dist/opaque.d.mts +16 -10
  34. package/dist/opaque.d.mts.map +1 -1
  35. package/dist/opaque.mjs +1 -1
  36. package/dist/prisma.d.mts +99 -3
  37. package/dist/prisma.d.mts.map +1 -1
  38. package/dist/prisma.mjs +101 -3
  39. package/dist/prisma.mjs.map +1 -1
  40. package/dist/typeorm.d.mts +25 -1
  41. package/dist/typeorm.d.mts.map +1 -1
  42. package/dist/typeorm.mjs +35 -2
  43. package/dist/typeorm.mjs.map +1 -1
  44. package/package.json +1 -1
  45. package/dist/adapter-types-7wWdELSh.mjs.map +0 -1
  46. package/dist/opaque-COAcIIY4.mjs.map +0 -1
@@ -2,10 +2,12 @@ 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
4
  import { ConvertCustomConfig, PgCustomColumnBuilder } from "drizzle-orm/pg-core";
5
+ import { ConvertCustomConfig as ConvertCustomConfig$1, MySqlCustomColumnBuilder } from "drizzle-orm/mysql-core";
6
+ import { ConvertCustomConfig as ConvertCustomConfig$2, SQLiteCustomColumnBuilder } from "drizzle-orm/sqlite-core";
5
7
 
6
8
  //#region src/adapters/drizzle.d.ts
7
9
  /**
8
- * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.
10
+ * Drizzle custom column type that stores an `Id<Brand>` as a canonical SQL string value in PostgreSQL.
9
11
  *
10
12
  * **Write path:** passes the `Id<Brand>` directly to the driver — it is already
11
13
  * the canonical string form.
@@ -15,20 +17,102 @@ import { ConvertCustomConfig, PgCustomColumnBuilder } from "drizzle-orm/pg-core"
15
17
  * is a safe boundary in case stale non-canonical values exist. Throws if the
16
18
  * value from the database does not parse as a valid `Id<Brand>`.
17
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.
25
+ *
18
26
  * @example
19
27
  * ```ts
20
28
  * import { idColumn } from "@smonn/ids/drizzle";
21
29
  * import { createTimestampId } from "@smonn/ids";
22
30
  *
23
31
  * const usr = createTimestampId("usr");
32
+ * // default: text column
24
33
  * export const users = pgTable("users", { id: idColumn(usr).primaryKey() });
34
+ * // explicit varchar column
35
+ * export const orgs = pgTable("orgs", { id: idColumn(usr, { columnType: "varchar(30)" }).primaryKey() });
36
+ * ```
37
+ */
38
+ declare function idColumn<Brand extends string>(codec: IdColumnCodec<Brand>, options?: {
39
+ columnType?: string;
40
+ }): PgCustomColumnBuilder<ConvertCustomConfig<"", {
41
+ data: Id<Brand>;
42
+ driverData: string;
43
+ }>>;
44
+ /**
45
+ * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value in MySQL.
46
+ *
47
+ * **Write path:** passes the `Id<Brand>` directly to the driver — it is already
48
+ * the canonical string form.
49
+ *
50
+ * **Read path:** normalises the raw DB string via `codec.safeParse()`, not strict
51
+ * `is()`. Throws `IdsError("invalid_id")` if the value from the database does not
52
+ * parse as a valid `Id<Brand>`.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { idColumnMysql } from "@smonn/ids/drizzle";
57
+ * import { createTimestampId } from "@smonn/ids";
58
+ *
59
+ * const usr = createTimestampId("usr");
60
+ * export const users = mysqlTable("users", { id: idColumnMysql(usr).primaryKey() });
25
61
  * // users.id is Id<"usr"> end-to-end
26
62
  * ```
27
63
  */
28
- declare function idColumn<Brand extends string>(codec: IdColumnCodec<Brand>): PgCustomColumnBuilder<ConvertCustomConfig<"", {
64
+ declare function idColumnMysql<Brand extends string>(codec: IdColumnCodec<Brand>): MySqlCustomColumnBuilder<ConvertCustomConfig$1<"", {
29
65
  data: Id<Brand>;
30
66
  driverData: string;
31
67
  }>>;
68
+ /**
69
+ * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value in SQLite.
70
+ *
71
+ * **Write path:** passes the `Id<Brand>` directly to the driver — it is already
72
+ * the canonical string form.
73
+ *
74
+ * **Read path:** normalises the raw DB string via `codec.safeParse()`, not strict
75
+ * `is()`. Throws `IdsError("invalid_id")` if the value from the database does not
76
+ * parse as a valid `Id<Brand>`.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * import { idColumnSqlite } from "@smonn/ids/drizzle";
81
+ * import { createTimestampId } from "@smonn/ids";
82
+ *
83
+ * const usr = createTimestampId("usr");
84
+ * export const users = sqliteTable("users", { id: idColumnSqlite(usr).primaryKey() });
85
+ * // users.id is Id<"usr"> end-to-end
86
+ * ```
87
+ */
88
+ declare function idColumnSqlite<Brand extends string>(codec: IdColumnCodec<Brand>): SQLiteCustomColumnBuilder<ConvertCustomConfig$2<"", {
89
+ data: Id<Brand>;
90
+ driverData: string;
91
+ }>>;
92
+ /**
93
+ * Drizzle custom column type for a **nullable** `Id<Brand>` column.
94
+ *
95
+ * Behaves identically to {@link idColumn} except that `null` and `undefined`
96
+ * driver values are passed through as `null` rather than throwing. Use for
97
+ * optional foreign keys, `LEFT JOIN` results, and any column that is
98
+ * legitimately absent.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * import { nullableIdColumn } from "@smonn/ids/drizzle";
103
+ * import { createTimestampId } from "@smonn/ids";
104
+ *
105
+ * const usr = createTimestampId("usr");
106
+ * export const posts = pgTable("posts", {
107
+ * authorId: nullableIdColumn(usr),
108
+ * });
109
+ * // posts.authorId is Id<"usr"> | null end-to-end
110
+ * ```
111
+ */
112
+ declare function nullableIdColumn<Brand extends string>(codec: IdColumnCodec<Brand>): PgCustomColumnBuilder<ConvertCustomConfig<"", {
113
+ data: Id<Brand> | null;
114
+ driverData: string | null;
115
+ }>>;
32
116
  //#endregion
33
- export { type IdColumnCodec, IdsError, type IdsErrorCode, idColumn, isIdsError };
117
+ export { type IdColumnCodec, IdsError, type IdsErrorCode, idColumn, idColumnMysql, idColumnSqlite, isIdsError, nullableIdColumn };
34
118
  //# sourceMappingURL=drizzle.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle.d.mts","names":[],"sources":["../src/adapters/drizzle.ts"],"mappings":";;;;;;AAkCA;;;;;;;;;;;;;;;;;;;;;AAAA,iBAAgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,qBAAA,CAAsB,mBAAA;EAA0B,IAAA,EAAM,EAAA,CAAG,KAAA;EAAQ,UAAA;AAAA"}
1
+ {"version":3,"file":"drizzle.d.mts","names":[],"sources":["../src/adapters/drizzle.ts"],"mappings":";;;;;;;;AAoDA;;;;;;;;;;;;;;;;;;;;;;;;AAGoE;AAmCpE;;;;AAtCA,iBAAgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,GACrB,OAAA;EAAY,UAAA;AAAA,IACX,qBAAA,CAAsB,mBAAA;EAA0B,IAAA,EAAM,EAAA,CAAG,KAAA;EAAQ,UAAA;AAAA;;;;;;;;;;;;;AAqCQ;AAkC5E;;;;;;;iBApCgB,aAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,wBAAA,CAAyB,qBAAA;EAA+B,IAAA,EAAM,EAAA,CAAG,KAAA;EAAQ,UAAA;AAAA;;;;;;;;;;;;AAqCzB;AAmCnD;;;;;;;;iBAtCgB,cAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,yBAAA,CACD,qBAAA;EAAgC,IAAA,EAAM,EAAA,CAAG,KAAA;EAAQ,UAAA;AAAA;;;;;;;;;;;AAsCC;;;;;;;;;;iBAHpC,gBAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,qBAAA,CACD,mBAAA;EAA0B,IAAA,EAAM,EAAA,CAAG,KAAA;EAAe,UAAA;AAAA"}
package/dist/drizzle.mjs CHANGED
@@ -1,9 +1,11 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
2
+ import { n as readIdColumnNullable, t as readIdColumn } from "./adapter-types-CjzFNDcJ.mjs";
3
3
  import { customType } from "drizzle-orm/pg-core";
4
+ import { customType as customType$1 } from "drizzle-orm/mysql-core";
5
+ import { customType as customType$2 } from "drizzle-orm/sqlite-core";
4
6
  //#region src/adapters/drizzle.ts
5
7
  /**
6
- * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.
8
+ * Drizzle custom column type that stores an `Id<Brand>` as a canonical SQL string value in PostgreSQL.
7
9
  *
8
10
  * **Write path:** passes the `Id<Brand>` directly to the driver — it is already
9
11
  * the canonical string form.
@@ -13,18 +15,93 @@ import { customType } from "drizzle-orm/pg-core";
13
15
  * is a safe boundary in case stale non-canonical values exist. Throws if the
14
16
  * value from the database does not parse as a valid `Id<Brand>`.
15
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.
23
+ *
16
24
  * @example
17
25
  * ```ts
18
26
  * import { idColumn } from "@smonn/ids/drizzle";
19
27
  * import { createTimestampId } from "@smonn/ids";
20
28
  *
21
29
  * const usr = createTimestampId("usr");
30
+ * // default: text column
22
31
  * export const users = pgTable("users", { id: idColumn(usr).primaryKey() });
23
- * // users.id is Id<"usr"> end-to-end
32
+ * // explicit varchar column
33
+ * export const orgs = pgTable("orgs", { id: idColumn(usr, { columnType: "varchar(30)" }).primaryKey() });
24
34
  * ```
25
35
  */
26
- function idColumn(codec) {
36
+ function idColumn(codec, options) {
37
+ const columnType = options?.columnType ?? "text";
27
38
  return customType({
39
+ dataType() {
40
+ return columnType;
41
+ },
42
+ toDriver(value) {
43
+ return value;
44
+ },
45
+ fromDriver(value) {
46
+ return readIdColumn(codec, value);
47
+ }
48
+ })();
49
+ }
50
+ /**
51
+ * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value in MySQL.
52
+ *
53
+ * **Write path:** passes the `Id<Brand>` directly to the driver — it is already
54
+ * the canonical string form.
55
+ *
56
+ * **Read path:** normalises the raw DB string via `codec.safeParse()`, not strict
57
+ * `is()`. Throws `IdsError("invalid_id")` if the value from the database does not
58
+ * parse as a valid `Id<Brand>`.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * import { idColumnMysql } from "@smonn/ids/drizzle";
63
+ * import { createTimestampId } from "@smonn/ids";
64
+ *
65
+ * const usr = createTimestampId("usr");
66
+ * export const users = mysqlTable("users", { id: idColumnMysql(usr).primaryKey() });
67
+ * // users.id is Id<"usr"> end-to-end
68
+ * ```
69
+ */
70
+ function idColumnMysql(codec) {
71
+ return customType$1({
72
+ dataType() {
73
+ return "text";
74
+ },
75
+ toDriver(value) {
76
+ return value;
77
+ },
78
+ fromDriver(value) {
79
+ return readIdColumn(codec, value);
80
+ }
81
+ })();
82
+ }
83
+ /**
84
+ * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value in SQLite.
85
+ *
86
+ * **Write path:** passes the `Id<Brand>` directly to the driver — it is already
87
+ * the canonical string form.
88
+ *
89
+ * **Read path:** normalises the raw DB string via `codec.safeParse()`, not strict
90
+ * `is()`. Throws `IdsError("invalid_id")` if the value from the database does not
91
+ * parse as a valid `Id<Brand>`.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * import { idColumnSqlite } from "@smonn/ids/drizzle";
96
+ * import { createTimestampId } from "@smonn/ids";
97
+ *
98
+ * const usr = createTimestampId("usr");
99
+ * export const users = sqliteTable("users", { id: idColumnSqlite(usr).primaryKey() });
100
+ * // users.id is Id<"usr"> end-to-end
101
+ * ```
102
+ */
103
+ function idColumnSqlite(codec) {
104
+ return customType$2({
28
105
  dataType() {
29
106
  return "text";
30
107
  },
@@ -36,7 +113,40 @@ function idColumn(codec) {
36
113
  }
37
114
  })();
38
115
  }
116
+ /**
117
+ * Drizzle custom column type for a **nullable** `Id<Brand>` column.
118
+ *
119
+ * Behaves identically to {@link idColumn} except that `null` and `undefined`
120
+ * driver values are passed through as `null` rather than throwing. Use for
121
+ * optional foreign keys, `LEFT JOIN` results, and any column that is
122
+ * legitimately absent.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import { nullableIdColumn } from "@smonn/ids/drizzle";
127
+ * import { createTimestampId } from "@smonn/ids";
128
+ *
129
+ * const usr = createTimestampId("usr");
130
+ * export const posts = pgTable("posts", {
131
+ * authorId: nullableIdColumn(usr),
132
+ * });
133
+ * // posts.authorId is Id<"usr"> | null end-to-end
134
+ * ```
135
+ */
136
+ function nullableIdColumn(codec) {
137
+ return customType({
138
+ dataType() {
139
+ return "text";
140
+ },
141
+ toDriver(value) {
142
+ return value;
143
+ },
144
+ fromDriver(value) {
145
+ return readIdColumnNullable(codec, value);
146
+ }
147
+ })();
148
+ }
39
149
  //#endregion
40
- export { IdsError, idColumn, isIdsError };
150
+ export { IdsError, idColumn, idColumnMysql, idColumnSqlite, isIdsError, nullableIdColumn };
41
151
 
42
152
  //# sourceMappingURL=drizzle.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle.mjs","names":[],"sources":["../src/adapters/drizzle.ts"],"sourcesContent":["import {\n customType,\n type ConvertCustomConfig,\n type PgCustomColumnBuilder,\n} from \"drizzle-orm/pg-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 * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.\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()`, not strict\n * `is()`. Data at rest should already be canonical per ADR-0003, but `safeParse`\n * is a safe boundary in case stale non-canonical values exist. Throws if the\n * value from the database does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/drizzle\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * export const users = pgTable(\"users\", { id: idColumn(usr).primaryKey() });\n * // users.id is Id<\"usr\"> end-to-end\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): PgCustomColumnBuilder<ConvertCustomConfig<\"\", { data: Id<Brand>; driverData: string }>> {\n return customType<{ data: Id<Brand>; driverData: string }>({\n dataType() {\n return \"text\";\n },\n toDriver(value: Id<Brand>): string {\n return value;\n },\n fromDriver(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n },\n })();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,SACd,OACyF;CACzF,OAAO,WAAoD;EACzD,WAAW;GACT,OAAO;EACT;EACA,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF,CAAC,CAAC,CAAC;AACL"}
1
+ {"version":3,"file":"drizzle.mjs","names":["customTypeMysql","customTypeSqlite"],"sources":["../src/adapters/drizzle.ts"],"sourcesContent":["import {\n customType,\n type ConvertCustomConfig,\n type PgCustomColumnBuilder,\n} from \"drizzle-orm/pg-core\";\nimport {\n customType as customTypeMysql,\n type ConvertCustomConfig as ConvertCustomConfigMysql,\n type MySqlCustomColumnBuilder,\n} from \"drizzle-orm/mysql-core\";\nimport {\n customType as customTypeSqlite,\n type ConvertCustomConfig as ConvertCustomConfigSqlite,\n type SQLiteCustomColumnBuilder,\n} from \"drizzle-orm/sqlite-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 * Drizzle custom column type that stores an `Id<Brand>` as a canonical SQL string value in PostgreSQL.\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()`, not strict\n * `is()`. Data at rest should already be canonical per ADR-0003, but `safeParse`\n * is a safe boundary in case stale non-canonical values exist. Throws if the\n * value from the database does not parse as a valid `Id<Brand>`.\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 { idColumn } from \"@smonn/ids/drizzle\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * // default: text column\n * export const users = pgTable(\"users\", { id: idColumn(usr).primaryKey() });\n * // explicit varchar column\n * export const orgs = pgTable(\"orgs\", { id: idColumn(usr, { columnType: \"varchar(30)\" }).primaryKey() });\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n options?: { columnType?: string },\n): PgCustomColumnBuilder<ConvertCustomConfig<\"\", { data: Id<Brand>; driverData: string }>> {\n const columnType = options?.columnType ?? \"text\";\n return customType<{ data: Id<Brand>; driverData: string }>({\n dataType() {\n return columnType;\n },\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 * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value in MySQL.\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()`, not strict\n * `is()`. Throws `IdsError(\"invalid_id\")` if the value from the database does not\n * parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumnMysql } from \"@smonn/ids/drizzle\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * export const users = mysqlTable(\"users\", { id: idColumnMysql(usr).primaryKey() });\n * // users.id is Id<\"usr\"> end-to-end\n * ```\n */\nexport function idColumnMysql<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): MySqlCustomColumnBuilder<ConvertCustomConfigMysql<\"\", { data: Id<Brand>; driverData: string }>> {\n return customTypeMysql<{ data: Id<Brand>; driverData: string }>({\n dataType() {\n return \"text\";\n },\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 * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value in SQLite.\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()`, not strict\n * `is()`. Throws `IdsError(\"invalid_id\")` if the value from the database does not\n * parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumnSqlite } from \"@smonn/ids/drizzle\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * export const users = sqliteTable(\"users\", { id: idColumnSqlite(usr).primaryKey() });\n * // users.id is Id<\"usr\"> end-to-end\n * ```\n */\nexport function idColumnSqlite<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): SQLiteCustomColumnBuilder<\n ConvertCustomConfigSqlite<\"\", { data: Id<Brand>; driverData: string }>\n> {\n return customTypeSqlite<{ data: Id<Brand>; driverData: string }>({\n dataType() {\n return \"text\";\n },\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 * Drizzle custom column type for a **nullable** `Id<Brand>` column.\n *\n * Behaves identically to {@link idColumn} except that `null` and `undefined`\n * driver values are passed through as `null` rather than throwing. Use for\n * optional foreign keys, `LEFT JOIN` results, and any column that is\n * legitimately absent.\n *\n * @example\n * ```ts\n * import { nullableIdColumn } from \"@smonn/ids/drizzle\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * export const posts = pgTable(\"posts\", {\n * authorId: nullableIdColumn(usr),\n * });\n * // posts.authorId is Id<\"usr\"> | null end-to-end\n * ```\n */\nexport function nullableIdColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): PgCustomColumnBuilder<\n ConvertCustomConfig<\"\", { data: Id<Brand> | null; driverData: string | null }>\n> {\n return customType<{ data: Id<Brand> | null; driverData: string | null }>({\n dataType() {\n return \"text\";\n },\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,SACd,OACA,SACyF;CACzF,MAAM,aAAa,SAAS,cAAc;CAC1C,OAAO,WAAoD;EACzD,WAAW;GACT,OAAO;EACT;EACA,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF,CAAC,CAAC,CAAC;AACL;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,cACd,OACiG;CACjG,OAAOA,aAAyD;EAC9D,WAAW;GACT,OAAO;EACT;EACA,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF,CAAC,CAAC,CAAC;AACL;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,eACd,OAGA;CACA,OAAOC,aAA0D;EAC/D,WAAW;GACT,OAAO;EACT;EACA,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF,CAAC,CAAC,CAAC;AACL;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,iBACd,OAGA;CACA,OAAO,WAAkE;EACvE,WAAW;GACT,OAAO;EACT;EACA,SAAS,OAAwC;GAC/C,OAAO;EACT;EACA,WAAW,OAAwC;GACjD,OAAO,qBAAqB,OAAO,KAAK;EAC1C;CACF,CAAC,CAAC,CAAC;AACL"}
@@ -12,7 +12,7 @@ declare class IdParamError extends Error {
12
12
  readonly reason: "brand_mismatch" | "malformed";
13
13
  constructor(reason: "brand_mismatch" | "malformed", status: number);
14
14
  }
15
- /** Options for `idParam`. All fields are optional. */
15
+ /** Options for `idParam` and `idQuery`. All fields are optional. */
16
16
  type IdParamOptions = {
17
17
  /**
18
18
  * Called instead of forwarding to `next(err)` when provided. The hook owns the response
@@ -77,6 +77,48 @@ type IdParamOptions = {
77
77
  * ```
78
78
  */
79
79
  declare function idParam<ParamKey extends string, Brand extends string>(paramName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): (req: Request, res: Response<unknown, Record<ParamKey, Id<Brand>>>, next: NextFunction) => void;
80
+ /**
81
+ * Express middleware that validates a named query-string param against a codec via `safeParse`.
82
+ *
83
+ * Same failure contract as `idParam` — same `IdParamOptions` / `IdParamFailure` shape, same
84
+ * `IdParamError` forwarded to `next(err)` — but reads `req.query[queryName]` instead of
85
+ * `req.params[queryName]`.
86
+ *
87
+ * **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and
88
+ * `reason`, so the app's existing error-handling middleware controls rendering. The adapter
89
+ * does not write a response body itself.
90
+ *
91
+ * **`options.onError`:** when provided, the hook owns the response entirely — the adapter does
92
+ * not call `next(err)`.
93
+ *
94
+ * **`options.status`:** remaps the default HTTP status for a reason without a full handler.
95
+ *
96
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
97
+ * - **Malformed or missing query param → `reason: "malformed"`, default 400**
98
+ *
99
+ * On success, stores the canonical `Id<Brand>` in `res.locals` under `queryName`
100
+ * and calls `next()`.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * import { idQuery, IdParamError } from "@smonn/ids/express";
105
+ * import { createTimestampId } from "@smonn/ids";
106
+ *
107
+ * const usr = createTimestampId("usr");
108
+ *
109
+ * // Default: forwards error to app error-handling middleware
110
+ * // GET /users?userId=usr_...
111
+ * app.get("/users", idQuery("userId", usr), (req, res) => {
112
+ * const userId = res.locals.userId; // Id<"usr">, canonical
113
+ * });
114
+ *
115
+ * // Override: consumer fully owns the response
116
+ * app.get("/search", idQuery("cursor", usr, {
117
+ * onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),
118
+ * }), handler);
119
+ * ```
120
+ */
121
+ declare function idQuery<ParamKey extends string, Brand extends string>(queryName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): (req: Request, res: Response<unknown, Record<ParamKey, Id<Brand>>>, next: NextFunction) => void;
80
122
  //#endregion
81
- export { IdParamError, type IdParamFailure, IdParamOptions, idParam };
123
+ export { IdParamError, type IdParamFailure, IdParamOptions, idParam, idQuery };
82
124
  //# sourceMappingURL=express.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"express.d.mts","names":[],"sources":["../src/adapters/express.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,MAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,MAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA;;;;;EAKvE,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;AAAA;AAmDtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAI6E;iBAJ7D,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IACR,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,UAAkB,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA,KAAU,IAAA,EAAM,YAAA"}
1
+ {"version":3,"file":"express.d.mts","names":[],"sources":["../src/adapters/express.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,MAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,MAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA;;;;;EAKvE,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;AAAA;AAmDtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAI6E;iBAJ7D,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IACR,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,UAAkB,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA,KAAU,IAAA,EAAM,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA;;;;;;;;;iBAJ7D,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IACR,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,UAAkB,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA,KAAU,IAAA,EAAM,YAAA"}
package/dist/express.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
1
+ import { r as resolveIdParamFailure } from "./adapter-types-CjzFNDcJ.mjs";
2
2
  //#region src/adapters/express.ts
3
3
  /**
4
4
  * Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.
@@ -79,7 +79,65 @@ function idParam(paramName, codec, options) {
79
79
  next();
80
80
  };
81
81
  }
82
+ /**
83
+ * Express middleware that validates a named query-string param against a codec via `safeParse`.
84
+ *
85
+ * Same failure contract as `idParam` — same `IdParamOptions` / `IdParamFailure` shape, same
86
+ * `IdParamError` forwarded to `next(err)` — but reads `req.query[queryName]` instead of
87
+ * `req.params[queryName]`.
88
+ *
89
+ * **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and
90
+ * `reason`, so the app's existing error-handling middleware controls rendering. The adapter
91
+ * does not write a response body itself.
92
+ *
93
+ * **`options.onError`:** when provided, the hook owns the response entirely — the adapter does
94
+ * not call `next(err)`.
95
+ *
96
+ * **`options.status`:** remaps the default HTTP status for a reason without a full handler.
97
+ *
98
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
99
+ * - **Malformed or missing query param → `reason: "malformed"`, default 400**
100
+ *
101
+ * On success, stores the canonical `Id<Brand>` in `res.locals` under `queryName`
102
+ * and calls `next()`.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * import { idQuery, IdParamError } from "@smonn/ids/express";
107
+ * import { createTimestampId } from "@smonn/ids";
108
+ *
109
+ * const usr = createTimestampId("usr");
110
+ *
111
+ * // Default: forwards error to app error-handling middleware
112
+ * // GET /users?userId=usr_...
113
+ * app.get("/users", idQuery("userId", usr), (req, res) => {
114
+ * const userId = res.locals.userId; // Id<"usr">, canonical
115
+ * });
116
+ *
117
+ * // Override: consumer fully owns the response
118
+ * app.get("/search", idQuery("cursor", usr, {
119
+ * onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),
120
+ * }), handler);
121
+ * ```
122
+ */
123
+ function idQuery(queryName, codec, options) {
124
+ return (req, res, next) => {
125
+ const raw = req.query[queryName];
126
+ const result = codec.safeParse(raw);
127
+ if (!result.ok) {
128
+ const failure = resolveIdParamFailure(result.error, options);
129
+ if (options?.onError) {
130
+ options.onError(failure, req, res, next);
131
+ return;
132
+ }
133
+ next(new IdParamError(failure.reason, failure.status));
134
+ return;
135
+ }
136
+ res.locals[queryName] = result.id;
137
+ next();
138
+ };
139
+ }
82
140
  //#endregion
83
- export { IdParamError, idParam };
141
+ export { IdParamError, idParam, idQuery };
84
142
 
85
143
  //# sourceMappingURL=express.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"express.mjs","names":[],"sources":["../src/adapters/express.ts"],"sourcesContent":["import type { NextFunction, Request, Response } from \"express\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.\n * Inspect `err.reason` and `err.status` in error-handling middleware.\n */\nexport class IdParamError extends Error {\n readonly status: number;\n readonly reason: \"brand_mismatch\" | \"malformed\";\n\n constructor(reason: \"brand_mismatch\" | \"malformed\", status: number) {\n super(`ID validation failed: ${reason}`);\n this.name = \"IdParamError\";\n this.reason = reason;\n this.status = status;\n }\n}\n\n/** Options for `idParam`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of forwarding to `next(err)` when provided. The hook owns the response\n * entirely — the adapter does not call `next(err)` itself.\n */\n onError?: (failure: IdParamFailure, req: Request, res: Response, next: NextFunction) => void;\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 * Express middleware that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and `reason`,\n * so the app's existing error-handling middleware controls rendering. The adapter does not write\n * a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter does\n * not call `next(err)`.\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 `res.locals` under `paramName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idParam, IdParamError } from \"@smonn/ids/express\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: forwards error to app error-handling middleware\n * app.get(\"/users/:id\", idParam(\"id\", usr), (req, res) => {\n * const id = res.locals.id; // Id<\"usr\">, canonical\n * });\n *\n * // Error-handling middleware receives the typed error\n * app.use((err, req, res, next) => {\n * if (err instanceof IdParamError) {\n * res.status(err.status).json({ error: err.reason });\n * return;\n * }\n * next(err);\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/orgs/:id\", idParam(\"id\", org, {\n * onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),\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): (req: Request, res: Response<unknown, Record<ParamKey, Id<Brand>>>, next: NextFunction) => void {\n return (req, res, next): void => {\n const raw = req.params[paramName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n options.onError(failure, req, res, next);\n return;\n }\n next(new IdParamError(failure.reason, failure.status));\n return;\n }\n (res.locals as Record<string, unknown>)[paramName] = result.id;\n next();\n };\n}\n"],"mappings":";;;;;;AAUA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,QAAwC,QAAgB;EAClE,MAAM,yBAAyB,QAAQ;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,SAAS;CAChB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,SAAgB,QACd,WACA,OACA,SACiG;CACjG,QAAQ,KAAK,KAAK,SAAe;EAC/B,MAAM,MAAM,IAAI,OAAO;EACvB,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,QAAQ,QAAQ,SAAS,KAAK,KAAK,IAAI;IACvC;GACF;GACA,KAAK,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM,CAAC;GACrD;EACF;EACA,IAAK,OAAmC,aAAa,OAAO;EAC5D,KAAK;CACP;AACF"}
1
+ {"version":3,"file":"express.mjs","names":[],"sources":["../src/adapters/express.ts"],"sourcesContent":["import type { NextFunction, Request, Response } from \"express\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.\n * Inspect `err.reason` and `err.status` in error-handling middleware.\n */\nexport class IdParamError extends Error {\n readonly status: number;\n readonly reason: \"brand_mismatch\" | \"malformed\";\n\n constructor(reason: \"brand_mismatch\" | \"malformed\", status: number) {\n super(`ID validation failed: ${reason}`);\n this.name = \"IdParamError\";\n this.reason = reason;\n this.status = status;\n }\n}\n\n/** Options for `idParam` and `idQuery`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of forwarding to `next(err)` when provided. The hook owns the response\n * entirely — the adapter does not call `next(err)` itself.\n */\n onError?: (failure: IdParamFailure, req: Request, res: Response, next: NextFunction) => void;\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 * Express middleware that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and `reason`,\n * so the app's existing error-handling middleware controls rendering. The adapter does not write\n * a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter does\n * not call `next(err)`.\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 `res.locals` under `paramName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idParam, IdParamError } from \"@smonn/ids/express\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: forwards error to app error-handling middleware\n * app.get(\"/users/:id\", idParam(\"id\", usr), (req, res) => {\n * const id = res.locals.id; // Id<\"usr\">, canonical\n * });\n *\n * // Error-handling middleware receives the typed error\n * app.use((err, req, res, next) => {\n * if (err instanceof IdParamError) {\n * res.status(err.status).json({ error: err.reason });\n * return;\n * }\n * next(err);\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/orgs/:id\", idParam(\"id\", org, {\n * onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),\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): (req: Request, res: Response<unknown, Record<ParamKey, Id<Brand>>>, next: NextFunction) => void {\n return (req, res, next): void => {\n const raw = req.params[paramName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n options.onError(failure, req, res, next);\n return;\n }\n next(new IdParamError(failure.reason, failure.status));\n return;\n }\n (res.locals as Record<string, unknown>)[paramName] = result.id;\n next();\n };\n}\n\n/**\n * Express middleware that validates a named query-string param against a codec via `safeParse`.\n *\n * Same failure contract as `idParam` — same `IdParamOptions` / `IdParamFailure` shape, same\n * `IdParamError` forwarded to `next(err)` — but reads `req.query[queryName]` instead of\n * `req.params[queryName]`.\n *\n * **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and\n * `reason`, so the app's existing error-handling middleware controls rendering. The adapter\n * does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter does\n * not call `next(err)`.\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 `res.locals` under `queryName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idQuery, IdParamError } from \"@smonn/ids/express\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: forwards error to app error-handling middleware\n * // GET /users?userId=usr_...\n * app.get(\"/users\", idQuery(\"userId\", usr), (req, res) => {\n * const userId = res.locals.userId; // Id<\"usr\">, canonical\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/search\", idQuery(\"cursor\", usr, {\n * onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),\n * }), handler);\n * ```\n */\nexport function idQuery<ParamKey extends string, Brand extends string>(\n queryName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): (req: Request, res: Response<unknown, Record<ParamKey, Id<Brand>>>, next: NextFunction) => void {\n return (req, res, next): void => {\n const raw = req.query[queryName] as string | undefined;\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n options.onError(failure, req, res, next);\n return;\n }\n next(new IdParamError(failure.reason, failure.status));\n return;\n }\n (res.locals as Record<string, unknown>)[queryName] = result.id;\n next();\n };\n}\n"],"mappings":";;;;;;AAUA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,QAAwC,QAAgB;EAClE,MAAM,yBAAyB,QAAQ;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,SAAS;CAChB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,SAAgB,QACd,WACA,OACA,SACiG;CACjG,QAAQ,KAAK,KAAK,SAAe;EAC/B,MAAM,MAAM,IAAI,OAAO;EACvB,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,QAAQ,QAAQ,SAAS,KAAK,KAAK,IAAI;IACvC;GACF;GACA,KAAK,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM,CAAC;GACrD;EACF;EACA,IAAK,OAAmC,aAAa,OAAO;EAC5D,KAAK;CACP;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,QACd,WACA,OACA,SACiG;CACjG,QAAQ,KAAK,KAAK,SAAe;EAC/B,MAAM,MAAM,IAAI,MAAM;EACtB,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,QAAQ,QAAQ,SAAS,KAAK,KAAK,IAAI;IACvC;GACF;GACA,KAAK,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM,CAAC;GACrD;EACF;EACA,IAAK,OAAmC,aAAa,OAAO;EAC5D,KAAK;CACP;AACF"}
@@ -12,7 +12,7 @@ declare class IdParamError extends Error {
12
12
  readonly reason: "brand_mismatch" | "malformed";
13
13
  constructor(reason: "brand_mismatch" | "malformed", statusCode: number);
14
14
  }
15
- /** Options for `idParam`. All fields are optional. */
15
+ /** Options for `idParam` and `idQuery`. All fields are optional. */
16
16
  type IdParamOptions = {
17
17
  /**
18
18
  * Called instead of throwing when provided. The hook owns the response entirely —
@@ -89,6 +89,53 @@ type IdParamOptions = {
89
89
  declare function idParam<ParamKey extends string, Brand extends string>(paramName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): (request: FastifyRequest<{
90
90
  Params: Record<string, Id<Brand>>;
91
91
  }>, reply: FastifyReply) => Promise<void>;
92
+ /**
93
+ * Fastify `preHandler` hook factory that validates a named query-string param against a codec
94
+ * via `safeParse`.
95
+ *
96
+ * Same failure contract as `idParam` — same `IdParamOptions` / `IdParamFailure` shape, same
97
+ * `IdParamError` thrown into `setErrorHandler` — but reads `request.query[queryName]` instead of
98
+ * `request.params`.
99
+ *
100
+ * **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the
101
+ * app's existing `setErrorHandler` controls rendering. The adapter does not write a response
102
+ * body itself.
103
+ *
104
+ * **`options.onError`:** when provided, the hook calls `onError` and does not throw; the
105
+ * consumer fully owns the response via `reply`.
106
+ *
107
+ * **`options.status`:** remaps the default HTTP status for a reason without a full handler.
108
+ *
109
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
110
+ * - **Malformed or missing query param → `reason: "malformed"`, default 400**
111
+ *
112
+ * On success, stores the canonical `Id<Brand>` in `request.query` under `queryName`.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { idQuery, IdParamError } from "@smonn/ids/fastify";
117
+ * import { createTimestampId } from "@smonn/ids";
118
+ *
119
+ * const usr = createTimestampId("usr");
120
+ *
121
+ * // Default: throws IdParamError → setErrorHandler renders it
122
+ * // GET /users?userId=usr_...
123
+ * fastify.get("/users", { preHandler: idQuery("userId", usr) }, (request, reply) => {
124
+ * const userId = request.query.userId; // string (compile-time); Id<"usr"> at runtime
125
+ * });
126
+ *
127
+ * // Override: consumer fully owns the error response
128
+ * fastify.get("/search", {
129
+ * preHandler: idQuery("cursor", usr, {
130
+ * onError: (failure, request, reply) =>
131
+ * reply.status(failure.status).send({ error: failure.reason }),
132
+ * }),
133
+ * }, handler);
134
+ * ```
135
+ */
136
+ declare function idQuery<ParamKey extends string, Brand extends string>(queryName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): (request: FastifyRequest<{
137
+ Querystring: Record<string, Id<Brand>>;
138
+ }>, reply: FastifyReply) => Promise<void>;
92
139
  //#endregion
93
- export { IdParamError, type IdParamFailure, IdParamOptions, idParam };
140
+ export { IdParamError, type IdParamFailure, IdParamOptions, idParam, idQuery };
94
141
  //# sourceMappingURL=fastify.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fastify.d.mts","names":[],"sources":["../src/adapters/fastify.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,UAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,UAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IACE,OAAA,EAAS,cAAA,EACT,OAAA,EAAS,cAAA,EACT,KAAA,EAAO,YAAA,YACG,OAAA;;;;;EAKZ,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;AAAA;AA6DtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOK;;;;;;;;;;;;;;iBAPW,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IAEV,OAAA,EAAS,cAAA;EAAiB,MAAA,EAAQ,MAAA,SAAe,EAAA,CAAG,KAAA;AAAA,IACpD,KAAA,EAAO,YAAA,KACJ,OAAA"}
1
+ {"version":3,"file":"fastify.d.mts","names":[],"sources":["../src/adapters/fastify.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,UAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,UAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IACE,OAAA,EAAS,cAAA,EACT,OAAA,EAAS,cAAA,EACT,KAAA,EAAO,YAAA,YACG,OAAA;;;;;EAKZ,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;AAAA;AA6DtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOK;AA4DL;;;;;;;;;;;;;iBAnEgB,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IAEV,OAAA,EAAS,cAAA;EAAiB,MAAA,EAAQ,MAAA,SAAe,EAAA,CAAG,KAAA;AAAA,IACpD,KAAA,EAAO,YAAA,KACJ,OAAA;;;;;;;;;;;;;;;;AAmEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAPW,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IAEV,OAAA,EAAS,cAAA;EAAiB,WAAA,EAAa,MAAA,SAAe,EAAA,CAAG,KAAA;AAAA,IACzD,KAAA,EAAO,YAAA,KACJ,OAAA"}
package/dist/fastify.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
1
+ import { r as resolveIdParamFailure } from "./adapter-types-CjzFNDcJ.mjs";
2
2
  //#region src/adapters/fastify.ts
3
3
  /**
4
4
  * Typed error thrown into Fastify's `setErrorHandler` on validation failure.
@@ -87,7 +87,66 @@ function idParam(paramName, codec, options) {
87
87
  request.params[paramName] = result.id;
88
88
  };
89
89
  }
90
+ /**
91
+ * Fastify `preHandler` hook factory that validates a named query-string param against a codec
92
+ * via `safeParse`.
93
+ *
94
+ * Same failure contract as `idParam` — same `IdParamOptions` / `IdParamFailure` shape, same
95
+ * `IdParamError` thrown into `setErrorHandler` — but reads `request.query[queryName]` instead of
96
+ * `request.params`.
97
+ *
98
+ * **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the
99
+ * app's existing `setErrorHandler` controls rendering. The adapter does not write a response
100
+ * body itself.
101
+ *
102
+ * **`options.onError`:** when provided, the hook calls `onError` and does not throw; the
103
+ * consumer fully owns the response via `reply`.
104
+ *
105
+ * **`options.status`:** remaps the default HTTP status for a reason without a full handler.
106
+ *
107
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
108
+ * - **Malformed or missing query param → `reason: "malformed"`, default 400**
109
+ *
110
+ * On success, stores the canonical `Id<Brand>` in `request.query` under `queryName`.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * import { idQuery, IdParamError } from "@smonn/ids/fastify";
115
+ * import { createTimestampId } from "@smonn/ids";
116
+ *
117
+ * const usr = createTimestampId("usr");
118
+ *
119
+ * // Default: throws IdParamError → setErrorHandler renders it
120
+ * // GET /users?userId=usr_...
121
+ * fastify.get("/users", { preHandler: idQuery("userId", usr) }, (request, reply) => {
122
+ * const userId = request.query.userId; // string (compile-time); Id<"usr"> at runtime
123
+ * });
124
+ *
125
+ * // Override: consumer fully owns the error response
126
+ * fastify.get("/search", {
127
+ * preHandler: idQuery("cursor", usr, {
128
+ * onError: (failure, request, reply) =>
129
+ * reply.status(failure.status).send({ error: failure.reason }),
130
+ * }),
131
+ * }, handler);
132
+ * ```
133
+ */
134
+ function idQuery(queryName, codec, options) {
135
+ return async (request, reply) => {
136
+ const raw = request.query[queryName];
137
+ const result = codec.safeParse(raw);
138
+ if (!result.ok) {
139
+ const failure = resolveIdParamFailure(result.error, options);
140
+ if (options?.onError) {
141
+ await options.onError(failure, request, reply);
142
+ return;
143
+ }
144
+ throw new IdParamError(failure.reason, failure.status);
145
+ }
146
+ request.query[queryName] = result.id;
147
+ };
148
+ }
90
149
  //#endregion
91
- export { IdParamError, idParam };
150
+ export { IdParamError, idParam, idQuery };
92
151
 
93
152
  //# sourceMappingURL=fastify.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"fastify.mjs","names":[],"sources":["../src/adapters/fastify.ts"],"sourcesContent":["import type { FastifyReply, FastifyRequest } from \"fastify\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Typed error thrown into Fastify's `setErrorHandler` on validation failure.\n * Inspect `err.reason` and `err.statusCode` in your error handler.\n */\nexport class IdParamError extends Error {\n readonly statusCode: number;\n readonly reason: \"brand_mismatch\" | \"malformed\";\n\n constructor(reason: \"brand_mismatch\" | \"malformed\", statusCode: number) {\n super(`ID validation failed: ${reason}`);\n this.name = \"IdParamError\";\n this.reason = reason;\n this.statusCode = statusCode;\n }\n}\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 does not throw.\n */\n onError?: (\n failure: IdParamFailure,\n request: FastifyRequest,\n reply: FastifyReply,\n ) => void | Promise<void>;\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 * Fastify `preHandler` hook factory that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the app's\n * existing `setErrorHandler` controls rendering. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook calls `onError` and does not throw; the consumer\n * fully owns the response via `reply`.\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 `request.params` under `paramName`.\n *\n * **Return type note:** the returned hook is typed as\n * `(request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>, reply: FastifyReply) => Promise<void>`.\n * Assigning it to a Fastify `preHandler` slot is backward-compatible (method-signature bivariance applies).\n * However, a locally-annotated variable typed as the bare `(request: FastifyRequest, reply: FastifyReply) => Promise<void>`\n * will produce a TypeScript error under `--strictFunctionTypes` because function parameter types are contravariant.\n * Use `preHandler` assignment or let TypeScript infer the type to avoid this.\n *\n * @example\n * ```ts\n * import { idParam, IdParamError } from \"@smonn/ids/fastify\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws IdParamError → setErrorHandler renders it\n * fastify.get(\"/users/:id\", { preHandler: idParam(\"id\", usr) }, (request, reply) => {\n * const id = request.params.id; // string (compile-time); Id<\"usr\"> at runtime after preHandler\n * });\n *\n * // Error handler receives the typed error\n * fastify.setErrorHandler((err, request, reply) => {\n * if (err instanceof IdParamError) {\n * reply.status(err.statusCode).send({ error: err.reason });\n * return;\n * }\n * reply.send(err);\n * });\n *\n * // Override: consumer fully owns the error response\n * fastify.get(\"/orgs/:id\", {\n * preHandler: idParam(\"id\", org, {\n * onError: (failure, request, reply) =>\n * reply.status(failure.status).send({ error: failure.reason }),\n * }),\n * }, handler);\n *\n * // Or a lightweight status remap without a full handler\n * fastify.get(\"/things/:id\", {\n * preHandler: idParam(\"id\", thing, { status: { brand_mismatch: 400 } }),\n * }, handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): (\n request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>,\n reply: FastifyReply,\n) => Promise<void> {\n return async (request, reply): Promise<void> => {\n const raw = request.params[paramName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n await options.onError(failure, request, reply);\n return;\n }\n throw new IdParamError(failure.reason, failure.status);\n }\n request.params[paramName] = result.id;\n };\n}\n"],"mappings":";;;;;;AAUA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,QAAwC,YAAoB;EACtE,MAAM,yBAAyB,QAAQ;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,aAAa;CACpB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,SAAgB,QACd,WACA,OACA,SAIiB;CACjB,OAAO,OAAO,SAAS,UAAyB;EAC9C,MAAM,MAAM,QAAQ,OAAO;EAC3B,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,MAAM,QAAQ,QAAQ,SAAS,SAAS,KAAK;IAC7C;GACF;GACA,MAAM,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM;EACvD;EACA,QAAQ,OAAO,aAAa,OAAO;CACrC;AACF"}
1
+ {"version":3,"file":"fastify.mjs","names":[],"sources":["../src/adapters/fastify.ts"],"sourcesContent":["import type { FastifyReply, FastifyRequest } from \"fastify\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Typed error thrown into Fastify's `setErrorHandler` on validation failure.\n * Inspect `err.reason` and `err.statusCode` in your error handler.\n */\nexport class IdParamError extends Error {\n readonly statusCode: number;\n readonly reason: \"brand_mismatch\" | \"malformed\";\n\n constructor(reason: \"brand_mismatch\" | \"malformed\", statusCode: number) {\n super(`ID validation failed: ${reason}`);\n this.name = \"IdParamError\";\n this.reason = reason;\n this.statusCode = statusCode;\n }\n}\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 does not throw.\n */\n onError?: (\n failure: IdParamFailure,\n request: FastifyRequest,\n reply: FastifyReply,\n ) => void | Promise<void>;\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 * Fastify `preHandler` hook factory that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the app's\n * existing `setErrorHandler` controls rendering. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook calls `onError` and does not throw; the consumer\n * fully owns the response via `reply`.\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 `request.params` under `paramName`.\n *\n * **Return type note:** the returned hook is typed as\n * `(request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>, reply: FastifyReply) => Promise<void>`.\n * Assigning it to a Fastify `preHandler` slot is backward-compatible (method-signature bivariance applies).\n * However, a locally-annotated variable typed as the bare `(request: FastifyRequest, reply: FastifyReply) => Promise<void>`\n * will produce a TypeScript error under `--strictFunctionTypes` because function parameter types are contravariant.\n * Use `preHandler` assignment or let TypeScript infer the type to avoid this.\n *\n * @example\n * ```ts\n * import { idParam, IdParamError } from \"@smonn/ids/fastify\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws IdParamError → setErrorHandler renders it\n * fastify.get(\"/users/:id\", { preHandler: idParam(\"id\", usr) }, (request, reply) => {\n * const id = request.params.id; // string (compile-time); Id<\"usr\"> at runtime after preHandler\n * });\n *\n * // Error handler receives the typed error\n * fastify.setErrorHandler((err, request, reply) => {\n * if (err instanceof IdParamError) {\n * reply.status(err.statusCode).send({ error: err.reason });\n * return;\n * }\n * reply.send(err);\n * });\n *\n * // Override: consumer fully owns the error response\n * fastify.get(\"/orgs/:id\", {\n * preHandler: idParam(\"id\", org, {\n * onError: (failure, request, reply) =>\n * reply.status(failure.status).send({ error: failure.reason }),\n * }),\n * }, handler);\n *\n * // Or a lightweight status remap without a full handler\n * fastify.get(\"/things/:id\", {\n * preHandler: idParam(\"id\", thing, { status: { brand_mismatch: 400 } }),\n * }, handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): (\n request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>,\n reply: FastifyReply,\n) => Promise<void> {\n return async (request, reply): Promise<void> => {\n const raw = request.params[paramName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n await options.onError(failure, request, reply);\n return;\n }\n throw new IdParamError(failure.reason, failure.status);\n }\n request.params[paramName] = result.id;\n };\n}\n\n/**\n * Fastify `preHandler` hook factory that validates a named query-string param against a codec\n * via `safeParse`.\n *\n * Same failure contract as `idParam` — same `IdParamOptions` / `IdParamFailure` shape, same\n * `IdParamError` thrown into `setErrorHandler` — but reads `request.query[queryName]` instead of\n * `request.params`.\n *\n * **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the\n * app's existing `setErrorHandler` controls rendering. The adapter does not write a response\n * body itself.\n *\n * **`options.onError`:** when provided, the hook calls `onError` and does not throw; the\n * consumer fully owns the response via `reply`.\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 `request.query` under `queryName`.\n *\n * @example\n * ```ts\n * import { idQuery, IdParamError } from \"@smonn/ids/fastify\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws IdParamError → setErrorHandler renders it\n * // GET /users?userId=usr_...\n * fastify.get(\"/users\", { preHandler: idQuery(\"userId\", usr) }, (request, reply) => {\n * const userId = request.query.userId; // string (compile-time); Id<\"usr\"> at runtime\n * });\n *\n * // Override: consumer fully owns the error response\n * fastify.get(\"/search\", {\n * preHandler: idQuery(\"cursor\", usr, {\n * onError: (failure, request, reply) =>\n * reply.status(failure.status).send({ error: failure.reason }),\n * }),\n * }, handler);\n * ```\n */\nexport function idQuery<ParamKey extends string, Brand extends string>(\n queryName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): (\n request: FastifyRequest<{ Querystring: Record<string, Id<Brand>> }>,\n reply: FastifyReply,\n) => Promise<void> {\n return async (request, reply): Promise<void> => {\n const raw = request.query[queryName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n await options.onError(failure, request, reply);\n return;\n }\n throw new IdParamError(failure.reason, failure.status);\n }\n request.query[queryName] = result.id;\n };\n}\n"],"mappings":";;;;;;AAUA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,QAAwC,YAAoB;EACtE,MAAM,yBAAyB,QAAQ;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,aAAa;CACpB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,SAAgB,QACd,WACA,OACA,SAIiB;CACjB,OAAO,OAAO,SAAS,UAAyB;EAC9C,MAAM,MAAM,QAAQ,OAAO;EAC3B,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,MAAM,QAAQ,QAAQ,SAAS,SAAS,KAAK;IAC7C;GACF;GACA,MAAM,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM;EACvD;EACA,QAAQ,OAAO,aAAa,OAAO;CACrC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,QACd,WACA,OACA,SAIiB;CACjB,OAAO,OAAO,SAAS,UAAyB;EAC9C,MAAM,MAAM,QAAQ,MAAM;EAC1B,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,MAAM,QAAQ,QAAQ,SAAS,SAAS,KAAK;IAC7C;GACF;GACA,MAAM,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM;EACvD;EACA,QAAQ,MAAM,aAAa,OAAO;CACpC;AACF"}