@smonn/ids 0.5.0 → 0.7.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 +555 -18
- package/dist/adapter-types-oHCCSgOO.d.mts +12 -0
- package/dist/adapter-types-oHCCSgOO.d.mts.map +1 -0
- package/dist/cli.mjs +246 -63
- package/dist/cli.mjs.map +1 -1
- package/dist/{codec-shell-dWpxoFmy.mjs → codec-shell-DH-UO4UR.mjs} +8 -8
- package/dist/codec-shell-DH-UO4UR.mjs.map +1 -0
- package/dist/drizzle-CeSni5PB.d.mts +44 -0
- package/dist/drizzle-CeSni5PB.d.mts.map +1 -0
- package/dist/drizzle.d.mts +3 -0
- package/dist/drizzle.mjs +43 -0
- package/dist/drizzle.mjs.map +1 -0
- package/dist/error-Cp5qYZcv.mjs +52 -0
- package/dist/error-Cp5qYZcv.mjs.map +1 -0
- package/dist/error-DTr4i6Ic.d.mts +44 -0
- package/dist/error-DTr4i6Ic.d.mts.map +1 -0
- package/dist/express.d.mts +85 -0
- package/dist/express.d.mts.map +1 -0
- package/dist/express.mjs +90 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +88 -0
- package/dist/fastify.d.mts.map +1 -0
- package/dist/fastify.mjs +91 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +68 -0
- package/dist/hono.d.mts.map +1 -0
- package/dist/hono.mjs +63 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -2
- package/dist/kysely.d.mts +56 -0
- package/dist/kysely.d.mts.map +1 -0
- package/dist/kysely.mjs +43 -0
- package/dist/kysely.mjs.map +1 -0
- package/dist/{opaque-B4ps7Pqk.mjs → opaque-uvjOFY_0.mjs} +37 -20
- package/dist/opaque-uvjOFY_0.mjs.map +1 -0
- package/dist/opaque.d.mts +34 -9
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +3 -2
- package/dist/prisma.d.mts +85 -0
- package/dist/prisma.d.mts.map +1 -0
- package/dist/prisma.mjs +54 -0
- package/dist/prisma.mjs.map +1 -0
- package/dist/reverse-BgFU6JHw.mjs +87 -0
- package/dist/reverse-BgFU6JHw.mjs.map +1 -0
- package/dist/reverse.d.mts +77 -0
- package/dist/reverse.d.mts.map +1 -0
- package/dist/reverse.mjs +3 -0
- package/dist/signed.d.mts +56 -0
- package/dist/signed.d.mts.map +1 -0
- package/dist/signed.mjs +100 -0
- package/dist/signed.mjs.map +1 -0
- package/dist/{timestamp-Bgzxx8bE.mjs → timestamp-B5_UCzc6.mjs} +3 -3
- package/dist/{timestamp-Bgzxx8bE.mjs.map → timestamp-B5_UCzc6.mjs.map} +1 -1
- package/dist/{timestamp-bytes-B57RM7Ho.mjs → timestamp-bytes-BBY7JI33.mjs} +2 -2
- package/dist/{timestamp-bytes-B57RM7Ho.mjs.map → timestamp-bytes-BBY7JI33.mjs.map} +1 -1
- package/dist/wrapped-0vL72Nje.mjs +361 -0
- package/dist/wrapped-0vL72Nje.mjs.map +1 -0
- package/dist/wrapped.d.mts +89 -9
- package/dist/wrapped.d.mts.map +1 -1
- package/dist/wrapped.mjs +3 -336
- package/package.json +45 -3
- package/dist/codec-shell-dWpxoFmy.mjs.map +0 -1
- package/dist/opaque-B4ps7Pqk.mjs.map +0 -1
- package/dist/wrapped.mjs.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
1
2
|
//#region src/brand.ts
|
|
2
3
|
const brandPattern = /^[a-z]{3}$/;
|
|
3
4
|
/** Validates a three-character lowercase brand. Throws on invalid input. */
|
|
4
5
|
function validateBrand(brand) {
|
|
5
|
-
if (!brandPattern.test(brand)) throw new
|
|
6
|
+
if (!brandPattern.test(brand)) throw new IdsError("invalid_brand", "invalid brand: expected three lowercase a-z characters");
|
|
6
7
|
}
|
|
7
8
|
//#endregion
|
|
8
9
|
//#region src/base32.ts
|
|
@@ -103,11 +104,6 @@ function safeParse(prefix, value) {
|
|
|
103
104
|
id: prefix + base32
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
|
-
function parse(prefix, value) {
|
|
107
|
-
const result = safeParse(prefix, value);
|
|
108
|
-
if (result.ok) return result.id;
|
|
109
|
-
throw new Error(`Invalid ID: ${result.error}`);
|
|
110
|
-
}
|
|
111
107
|
function is(prefix, value) {
|
|
112
108
|
if (typeof value !== "string") return false;
|
|
113
109
|
if (!value.startsWith(prefix)) return false;
|
|
@@ -131,7 +127,11 @@ function standardValidate(prefix, value) {
|
|
|
131
127
|
function wireMethods(prefix) {
|
|
132
128
|
return {
|
|
133
129
|
is: (value) => is(prefix, value),
|
|
134
|
-
parse: (value) =>
|
|
130
|
+
parse: (value) => {
|
|
131
|
+
const result = safeParse(prefix, value);
|
|
132
|
+
if (result.ok) return result.id;
|
|
133
|
+
throw new IdsError("invalid_id", `invalid ID: ${result.error}`, { cause: result.error });
|
|
134
|
+
},
|
|
135
135
|
safeParse: (value) => safeParse(prefix, value),
|
|
136
136
|
toJsonSchema: (brand, example) => ({
|
|
137
137
|
type: "string",
|
|
@@ -149,4 +149,4 @@ function wireMethods(prefix) {
|
|
|
149
149
|
//#endregion
|
|
150
150
|
export { toWireId as a, payloadBytesFromId as i, registerBrand as n, decodeBase32 as o, payloadBase32Length as r, validateBrand as s, wireMethods as t };
|
|
151
151
|
|
|
152
|
-
//# sourceMappingURL=codec-shell-
|
|
152
|
+
//# sourceMappingURL=codec-shell-DH-UO4UR.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec-shell-DH-UO4UR.mjs","names":[],"sources":["../src/brand.ts","../src/base32.ts","../src/wire/envelope.ts","../src/wire/invariants.ts","../src/registry.ts","../src/wire/parse.ts","../src/wire/codec-shell.ts"],"sourcesContent":["import { IdsError } from \"./error.js\";\n\nconst brandPattern = /^[a-z]{3}$/;\n\n/** Validates a three-character lowercase brand. Throws on invalid input. */\nexport function validateBrand(brand: string): void {\n if (!brandPattern.test(brand)) {\n throw new IdsError(\"invalid_brand\", \"invalid brand: expected three lowercase a-z characters\");\n }\n}\n","/*\n This is based on Crockford's Base32 spec: https://www.crockford.com/base32.html\n One difference is that it uses lowercase instead of uppercase when encoding.\n\n These functions are internal: codec constructors guarantee that input is a\n 16-byte buffer for encode, or a string of characters drawn from the alphabet\n for decode. Invalid input produces silent garbage rather than a thrown error,\n consistent with the trust-the-type rule in ADR-0003.\n*/\n\nexport const alphabet = \"0123456789abcdefghjkmnpqrstvwxyz\";\n\n// 0–31 → ASCII char code, for write-into-codes-then-fromCharCode encoding.\nconst valueToCharCode = new Uint8Array(32);\nfor (let i = 0; i < 32; i++) valueToCharCode[i] = alphabet.charCodeAt(i);\n\n// charCode → 0–31 value. Canonical lowercase only; upstream resolves case and\n// o/i/l aliases before any string reaches decodeBase32.\nconst INVALID = 0xff;\nconst charCodeToValue = new Uint8Array(256).fill(INVALID);\nfor (let i = 0; i < alphabet.length; i++) charCodeToValue[alphabet.charCodeAt(i)] = i;\n\nexport function encodeBase32(bytes: Uint8Array): string {\n // Build an Array<number> of char codes and pass it to fromCharCode.apply.\n // Faster than `result += char` (avoids cons-string overhead) and than\n // Uint8Array variants (apply has a fast path for plain Arrays).\n // oxlint-disable-next-line no-new-array\n const codes = new Array<number>(Math.floor((bytes.length * 8) / 5) + 1);\n let chi = 0;\n let bits = 0;\n let value = 0;\n\n for (let i = 0; i < bytes.length; i++) {\n value = (value << 8) | bytes[i]!;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n codes[chi++] = valueToCharCode[(value >>> bits) & 0x1f]!;\n }\n }\n codes[chi] = valueToCharCode[(value << (5 - bits)) & 0x1f]!;\n return String.fromCharCode.apply(null, codes);\n}\n\nexport function decodeBase32(str: string): Uint8Array {\n const result = new Uint8Array(Math.floor((str.length * 5) / 8));\n let bits = 0;\n let value = 0;\n let index = 0;\n\n for (let i = 0; i < str.length; i++) {\n const v = charCodeToValue[str.charCodeAt(i)]!;\n value = (value << 5) | v;\n bits += 5;\n if (bits >= 8) {\n bits -= 8;\n result[index++] = (value >>> bits) & 0xff;\n }\n }\n return result;\n}\n","import { decodeBase32, encodeBase32 } from \"../base32.js\";\nimport type { Id, Prefix } from \"../types.js\";\n\n/** Encodes a 16-byte payload as lowercase Crockford base32 (26 chars). */\nfunction encodePayload(bytes: Uint8Array): string {\n return encodeBase32(bytes);\n}\n\n/** Decodes a 26-char base32 payload suffix to 16 bytes. Trust-the-type. */\nfunction decodePayload(base32: string): Uint8Array {\n return decodeBase32(base32);\n}\n\n/** Composes a canonical wire ID from a prefix and 16-byte payload. */\nexport function toWireId<Brand extends string>(\n prefix: Prefix<Brand>,\n payload: Uint8Array,\n): Id<Brand> {\n return (prefix + encodePayload(payload)) as Id<Brand>;\n}\n\n/** Decodes the full 16-byte payload from a trusted wire ID. */\nexport function payloadBytesFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n id: Id<Brand>,\n): Uint8Array {\n return decodePayload(id.slice(prefix.length));\n}\n","// Payload is always 16 bytes on the wire (every codec). 16 bytes → 26 Crockford\n// base32 chars. ADR-0002 codifies this as the shared wire-format invariant.\nexport const payloadByteLength: number = 16;\nexport const payloadBase32Length: number = Math.ceil((payloadByteLength * 8) / 5);\n\n// Compact regex character class for the canonical lowercase Crockford alphabet\n// (`0123456789abcdefghjkmnpqrstvwxyz` — excludes i, l, o, u). Used in the JSON\n// Schema `pattern`, which describes the canonical wire form only (ADR-0003).\nexport const base32CharClass: string = \"[0-9a-hjkmnp-tv-z]\";\n","const registeredBrands = new Set<string>();\nconst warnedBrands = new Set<string>();\n\nexport function registerBrand(brand: string, allowDuplicateBrand: boolean | undefined): void {\n if (\n typeof process === \"undefined\" ||\n process.env.NODE_ENV === \"production\" ||\n allowDuplicateBrand\n ) {\n return;\n }\n\n if (registeredBrands.has(brand)) {\n if (!warnedBrands.has(brand)) {\n console.warn(\n `[@smonn/ids] brand \"${brand}\" was registered more than once — this usually indicates a bundling or import bug, or that more than one codec variant is using the same brand. Pass { allowDuplicateBrand: true } to silence.`,\n );\n warnedBrands.add(brand);\n }\n } else {\n registeredBrands.add(brand);\n }\n}\n","import { alphabet } from \"../base32.js\";\nimport type { Id, ParseError, ParseResult, Prefix } from \"../types.js\";\nimport { payloadBase32Length } from \"./invariants.js\";\n\nconst replacePattern = /[ilo]/g;\nconst aliasTestPattern = /[ilo]/;\nconst replacer = (match: string): string => (match === \"o\" ? \"0\" : \"1\");\nconst base32Pattern = new RegExp(`^[${alphabet}]{${payloadBase32Length}}$`);\n\nexport function safeParse<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n): ParseResult<Brand> {\n if (typeof value !== \"string\") return { ok: false, error: \"not_string\" };\n const lowercase = value.toLowerCase();\n if (!lowercase.startsWith(prefix)) return { ok: false, error: \"invalid_prefix\" };\n\n const sliced = lowercase.slice(prefix.length);\n const base32 = aliasTestPattern.test(sliced)\n ? sliced.replaceAll(replacePattern, replacer)\n : sliced;\n\n if (!base32Pattern.test(base32)) return { ok: false, error: \"invalid_base32\" };\n\n const id = (prefix + base32) as Id<Brand>;\n return { ok: true, id };\n}\n\nexport function is<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n): value is Id<Brand> {\n if (typeof value !== \"string\") return false;\n if (!value.startsWith(prefix)) return false;\n return base32Pattern.test(value.slice(prefix.length));\n}\n\nfunction errorMessage<Brand extends string>(prefix: Prefix<Brand>, error: ParseError): string {\n switch (error) {\n case \"not_string\":\n return \"expected string\";\n case \"invalid_prefix\":\n return `expected prefix '${prefix}'`;\n case \"invalid_base32\":\n return \"invalid base32 payload\";\n }\n}\n\nexport function standardValidate<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n):\n | { readonly value: Id<Brand>; readonly issues?: undefined }\n | { readonly issues: ReadonlyArray<{ readonly message: string }> } {\n const result = safeParse(prefix, value);\n if (result.ok) return { value: result.id };\n return { issues: [{ message: errorMessage(prefix, result.error) }] };\n}\n","import { IdsError } from \"../error.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"../types.js\";\nimport { base32CharClass, payloadBase32Length } from \"./invariants.js\";\nimport { is, safeParse, standardValidate } from \"./parse.js\";\n\ntype WireMethods<Brand extends string> = {\n is: (value: unknown) => value is Id<Brand>;\n parse: (value: unknown) => Id<Brand>;\n safeParse: (value: unknown) => ParseResult<Brand>;\n toJsonSchema: (brand: Brand, example: string) => JsonSchema;\n \"~standard\": StandardSchemaProps<Brand>;\n};\n\n/** Wire-only methods shared by every codec variant for a fixed prefix. */\nexport function wireMethods<Brand extends string>(prefix: Prefix<Brand>): WireMethods<Brand> {\n const standard: StandardSchemaProps<Brand> = {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n };\n return {\n is: (value: unknown): value is Id<Brand> => is(prefix, value),\n parse: (value: unknown): Id<Brand> => {\n const result = safeParse(prefix, value);\n if (result.ok) return result.id;\n throw new IdsError(\"invalid_id\", `invalid ID: ${result.error}`, { cause: result.error });\n },\n safeParse: (value: unknown): ParseResult<Brand> => safeParse(prefix, value),\n toJsonSchema: (brand: Brand, example: string): JsonSchema => ({\n type: \"string\",\n pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,\n description: `Branded ID for '${brand}'`,\n example,\n }),\n \"~standard\": standard,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { i as ParseResult, t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
|
+
import { ConvertCustomConfig, PgCustomColumnBuilder } from "drizzle-orm/pg-core";
|
|
3
|
+
|
|
4
|
+
//#region src/drizzle.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Minimum codec interface required by the Drizzle adapter.
|
|
7
|
+
*
|
|
8
|
+
* Any codec variant satisfies this type — TimestampCodec, OpaqueTimestampCodec,
|
|
9
|
+
* and WrappedKeyCodec all expose `safeParse`. The adapter never calls
|
|
10
|
+
* `extractTimestamp`, `wrap`/`unwrap`, or any key-dependent method.
|
|
11
|
+
*
|
|
12
|
+
* Kysely and Prisma adapter issues should use this same codec contract shape.
|
|
13
|
+
*/
|
|
14
|
+
type IdColumnCodec<Brand extends string> = {
|
|
15
|
+
safeParse(value: unknown): ParseResult<Brand>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.
|
|
19
|
+
*
|
|
20
|
+
* **Write path:** passes the `Id<Brand>` directly to the driver — it is already
|
|
21
|
+
* the canonical string form.
|
|
22
|
+
*
|
|
23
|
+
* **Read path:** normalises the raw DB string via `codec.safeParse()`, not strict
|
|
24
|
+
* `is()`. Data at rest should already be canonical per ADR-0003, but `safeParse`
|
|
25
|
+
* is a safe boundary in case stale non-canonical values exist. Throws if the
|
|
26
|
+
* value from the database does not parse as a valid `Id<Brand>`.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { idColumn } from "@smonn/ids/drizzle";
|
|
31
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
32
|
+
*
|
|
33
|
+
* const usr = createTimestampId("usr");
|
|
34
|
+
* export const users = pgTable("users", { id: idColumn(usr).primaryKey() });
|
|
35
|
+
* // users.id is Id<"usr"> end-to-end
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function idColumn<Brand extends string>(codec: IdColumnCodec<Brand>): PgCustomColumnBuilder<ConvertCustomConfig<"", {
|
|
39
|
+
data: Id<Brand>;
|
|
40
|
+
driverData: string;
|
|
41
|
+
}>>;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { idColumn as n, IdColumnCodec as t };
|
|
44
|
+
//# sourceMappingURL=drizzle-CeSni5PB.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drizzle-CeSni5PB.d.mts","names":[],"sources":["../src/drizzle.ts"],"mappings":";;;;;;;;;;;;;KAoBY,aAAA;EACV,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;AA0B2B;iBAFpD,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,qBAAA,CAAsB,mBAAA;EAA0B,IAAA,EAAM,EAAA,CAAG,KAAA;EAAQ,UAAA;AAAA"}
|
package/dist/drizzle.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
+
import { customType } from "drizzle-orm/pg-core";
|
|
3
|
+
//#region src/drizzle.ts
|
|
4
|
+
/**
|
|
5
|
+
* Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.
|
|
6
|
+
*
|
|
7
|
+
* **Write path:** passes the `Id<Brand>` directly to the driver — it is already
|
|
8
|
+
* the canonical string form.
|
|
9
|
+
*
|
|
10
|
+
* **Read path:** normalises the raw DB string via `codec.safeParse()`, not strict
|
|
11
|
+
* `is()`. Data at rest should already be canonical per ADR-0003, but `safeParse`
|
|
12
|
+
* is a safe boundary in case stale non-canonical values exist. Throws if the
|
|
13
|
+
* value from the database does not parse as a valid `Id<Brand>`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { idColumn } from "@smonn/ids/drizzle";
|
|
18
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
19
|
+
*
|
|
20
|
+
* const usr = createTimestampId("usr");
|
|
21
|
+
* export const users = pgTable("users", { id: idColumn(usr).primaryKey() });
|
|
22
|
+
* // users.id is Id<"usr"> end-to-end
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function idColumn(codec) {
|
|
26
|
+
return customType({
|
|
27
|
+
dataType() {
|
|
28
|
+
return "text";
|
|
29
|
+
},
|
|
30
|
+
toDriver(value) {
|
|
31
|
+
return value;
|
|
32
|
+
},
|
|
33
|
+
fromDriver(value) {
|
|
34
|
+
const result = codec.safeParse(value);
|
|
35
|
+
if (!result.ok) throw new IdsError("invalid_id", `invalid ID from database: ${result.error}`, { cause: result.error });
|
|
36
|
+
return result.id;
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { IdsError, idColumn, isIdsError };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=drizzle.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drizzle.mjs","names":[],"sources":["../src/drizzle.ts"],"sourcesContent":["import {\n customType,\n type ConvertCustomConfig,\n type PgCustomColumnBuilder,\n} from \"drizzle-orm/pg-core\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport type { Id, ParseResult } 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\n/**\n * Minimum codec interface required by the Drizzle adapter.\n *\n * Any codec variant satisfies this type — TimestampCodec, OpaqueTimestampCodec,\n * and WrappedKeyCodec all expose `safeParse`. The adapter never calls\n * `extractTimestamp`, `wrap`/`unwrap`, or any key-dependent method.\n *\n * Kysely and Prisma adapter issues should use this same codec contract shape.\n */\nexport type IdColumnCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\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 const result = codec.safeParse(value);\n if (!result.ok) {\n throw new IdsError(\"invalid_id\", `invalid ID from database: ${result.error}`, {\n cause: result.error,\n });\n }\n return result.id;\n },\n })();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,SACd,OACyF;CACzF,OAAO,WAAoD;EACzD,WAAW;GACT,OAAO;EACT;EACA,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,MAAM,SAAS,MAAM,UAAU,KAAK;GACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,SAAS,cAAc,6BAA6B,OAAO,SAAS,EAC5E,OAAO,OAAO,MAChB,CAAC;GAEH,OAAO,OAAO;EAChB;CACF,CAAC,CAAC,CAAC;AACL"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/error.ts
|
|
2
|
+
const BRAND = Symbol.for("@smonn/ids/IdsError");
|
|
3
|
+
/**
|
|
4
|
+
* The single error class thrown by caller-reachable public failures.
|
|
5
|
+
* Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.
|
|
6
|
+
* Recognized via `isIdsError()` — a branded guard that survives realm/dual-package duplication
|
|
7
|
+
* where bare `instanceof` would silently fail.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* try {
|
|
12
|
+
* usr.parse(rawInput);
|
|
13
|
+
* } catch (err) {
|
|
14
|
+
* if (isIdsError(err) && err.code === "invalid_id") return; // handle parse failure
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
var IdsError = class extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code, message, options) {
|
|
21
|
+
super(message, options);
|
|
22
|
+
this.name = "IdsError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
Object.defineProperty(this, BRAND, {
|
|
25
|
+
value: true,
|
|
26
|
+
enumerable: false,
|
|
27
|
+
configurable: false,
|
|
28
|
+
writable: false
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Type guard for `IdsError`. Checks a non-enumerable brand rather than bare `instanceof`
|
|
34
|
+
* so it survives realm/dual-package duplication (ESM + CJS dual package hazard).
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* if (isIdsError(err)) {
|
|
39
|
+
* switch (err.code) {
|
|
40
|
+
* case "verification_failed": // ...
|
|
41
|
+
* case "invalid_id": // ...
|
|
42
|
+
* }
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function isIdsError(value) {
|
|
47
|
+
return typeof value === "object" && value !== null && value[BRAND] === true;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { isIdsError as n, IdsError as t };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=error-Cp5qYZcv.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-Cp5qYZcv.mjs","names":[],"sources":["../src/error.ts"],"sourcesContent":["const BRAND = Symbol.for(\"@smonn/ids/IdsError\");\n\n/**\n * The stable machine-readable failure reason carried by `IdsError`.\n * Use `code` — not `message` — for programmatic branching; `message` is non-contractual.\n * Adding a new member is minor-additive; renaming or removing one is breaking.\n */\nexport type IdsErrorCode =\n | \"invalid_brand\"\n | \"invalid_key_format\"\n | \"invalid_key_encoding\"\n | \"invalid_key_length\"\n | \"invalid_kind\"\n | \"empty_keyring\"\n | \"duplicate_keyring_entry\"\n | \"invalid_lookup_key\"\n | \"verification_failed\"\n | \"invalid_id\";\n\n/**\n * The single error class thrown by caller-reachable public failures.\n * Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.\n * Recognized via `isIdsError()` — a branded guard that survives realm/dual-package duplication\n * where bare `instanceof` would silently fail.\n *\n * @example\n * ```ts\n * try {\n * usr.parse(rawInput);\n * } catch (err) {\n * if (isIdsError(err) && err.code === \"invalid_id\") return; // handle parse failure\n * }\n * ```\n */\nexport class IdsError extends Error {\n readonly code: IdsErrorCode;\n\n constructor(code: IdsErrorCode, message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"IdsError\";\n this.code = code;\n Object.defineProperty(this, BRAND, {\n value: true,\n enumerable: false,\n configurable: false,\n writable: false,\n });\n }\n}\n\n/**\n * Type guard for `IdsError`. Checks a non-enumerable brand rather than bare `instanceof`\n * so it survives realm/dual-package duplication (ESM + CJS dual package hazard).\n *\n * @example\n * ```ts\n * if (isIdsError(err)) {\n * switch (err.code) {\n * case \"verification_failed\": // ...\n * case \"invalid_id\": // ...\n * }\n * }\n * ```\n */\nexport function isIdsError(value: unknown): value is IdsError {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Record<symbol, unknown>)[BRAND] === true\n );\n}\n"],"mappings":";AAAA,MAAM,QAAQ,OAAO,IAAI,qBAAqB;;;;;;;;;;;;;;;;AAkC9C,IAAa,WAAb,cAA8B,MAAM;CAClC;CAEA,YAAY,MAAoB,SAAiB,SAAwB;EACvE,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,OAAO,eAAe,MAAM,OAAO;GACjC,OAAO;GACP,YAAY;GACZ,cAAc;GACd,UAAU;EACZ,CAAC;CACH;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,WAAW,OAAmC;CAC5D,OACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,WAAW;AAElD"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/error.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The stable machine-readable failure reason carried by `IdsError`.
|
|
4
|
+
* Use `code` — not `message` — for programmatic branching; `message` is non-contractual.
|
|
5
|
+
* Adding a new member is minor-additive; renaming or removing one is breaking.
|
|
6
|
+
*/
|
|
7
|
+
type IdsErrorCode = "invalid_brand" | "invalid_key_format" | "invalid_key_encoding" | "invalid_key_length" | "invalid_kind" | "empty_keyring" | "duplicate_keyring_entry" | "invalid_lookup_key" | "verification_failed" | "invalid_id";
|
|
8
|
+
/**
|
|
9
|
+
* The single error class thrown by caller-reachable public failures.
|
|
10
|
+
* Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.
|
|
11
|
+
* Recognized via `isIdsError()` — a branded guard that survives realm/dual-package duplication
|
|
12
|
+
* where bare `instanceof` would silently fail.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* try {
|
|
17
|
+
* usr.parse(rawInput);
|
|
18
|
+
* } catch (err) {
|
|
19
|
+
* if (isIdsError(err) && err.code === "invalid_id") return; // handle parse failure
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare class IdsError extends Error {
|
|
24
|
+
readonly code: IdsErrorCode;
|
|
25
|
+
constructor(code: IdsErrorCode, message: string, options?: ErrorOptions);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Type guard for `IdsError`. Checks a non-enumerable brand rather than bare `instanceof`
|
|
29
|
+
* so it survives realm/dual-package duplication (ESM + CJS dual package hazard).
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* if (isIdsError(err)) {
|
|
34
|
+
* switch (err.code) {
|
|
35
|
+
* case "verification_failed": // ...
|
|
36
|
+
* case "invalid_id": // ...
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare function isIdsError(value: unknown): value is IdsError;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { IdsErrorCode as n, isIdsError as r, IdsError as t };
|
|
44
|
+
//# sourceMappingURL=error-DTr4i6Ic.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-DTr4i6Ic.d.mts","names":[],"sources":["../src/error.ts"],"mappings":";;AAOA;;;;KAAY,YAAA;AA2BZ;;;;;;;;;;;;;;;AAAA,cAAa,QAAA,SAAiB,KAAA;EAAA,SACnB,IAAA,EAAM,YAAA;EAEf,WAAA,CAAY,IAAA,EAAM,YAAA,EAAc,OAAA,UAAiB,OAAA,GAAU,YAAA;AAAA;AAAA;AA2B7D;;;;;;;;AAAqD;;;;;AA3BQ,iBA2B7C,UAAA,CAAW,KAAA,YAAiB,KAAA,IAAS,QAAA"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { i as ParseResult, t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
|
+
import { t as IdParamFailure } from "./adapter-types-oHCCSgOO.mjs";
|
|
3
|
+
import { NextFunction, Request, Response } from "express";
|
|
4
|
+
|
|
5
|
+
//#region src/express.d.ts
|
|
6
|
+
type IdCodec<Brand extends string> = {
|
|
7
|
+
safeParse(value: unknown): ParseResult<Brand>;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.
|
|
11
|
+
* Inspect `err.reason` and `err.status` in error-handling middleware.
|
|
12
|
+
*/
|
|
13
|
+
declare class IdParamError extends Error {
|
|
14
|
+
readonly status: number;
|
|
15
|
+
readonly reason: "brand_mismatch" | "malformed";
|
|
16
|
+
constructor(reason: "brand_mismatch" | "malformed", status: number);
|
|
17
|
+
}
|
|
18
|
+
/** Options for `idParam`. All fields are optional. */
|
|
19
|
+
type IdParamOptions = {
|
|
20
|
+
/**
|
|
21
|
+
* Called instead of forwarding to `next(err)` when provided. The hook owns the response
|
|
22
|
+
* entirely — the adapter does not call `next(err)` itself.
|
|
23
|
+
*/
|
|
24
|
+
onError?: (failure: IdParamFailure, req: Request, res: Response, next: NextFunction) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Remap the default HTTP status for a failure reason without a full handler.
|
|
27
|
+
* e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.
|
|
28
|
+
*/
|
|
29
|
+
status?: {
|
|
30
|
+
brand_mismatch?: number;
|
|
31
|
+
malformed?: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Express middleware that validates a named route param against a codec via `safeParse`.
|
|
36
|
+
*
|
|
37
|
+
* **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and `reason`,
|
|
38
|
+
* so the app's existing error-handling middleware controls rendering. The adapter does not write
|
|
39
|
+
* a response body itself.
|
|
40
|
+
*
|
|
41
|
+
* **`options.onError`:** when provided, the hook owns the response entirely — the adapter does
|
|
42
|
+
* not call `next(err)`.
|
|
43
|
+
*
|
|
44
|
+
* **`options.status`:** remaps the default HTTP status for a reason without a full handler.
|
|
45
|
+
*
|
|
46
|
+
* - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
|
|
47
|
+
* - **Malformed or missing ID → `reason: "malformed"`, default 400**
|
|
48
|
+
*
|
|
49
|
+
* On success, stores the canonical `Id<Brand>` in `res.locals` under `paramName`
|
|
50
|
+
* and calls `next()`.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { idParam, IdParamError } from "@smonn/ids/express";
|
|
55
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
56
|
+
*
|
|
57
|
+
* const usr = createTimestampId("usr");
|
|
58
|
+
*
|
|
59
|
+
* // Default: forwards error to app error-handling middleware
|
|
60
|
+
* app.get("/users/:id", idParam("id", usr), (req, res) => {
|
|
61
|
+
* const id = res.locals.id; // Id<"usr">, canonical
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* // Error-handling middleware receives the typed error
|
|
65
|
+
* app.use((err, req, res, next) => {
|
|
66
|
+
* if (err instanceof IdParamError) {
|
|
67
|
+
* res.status(err.status).json({ error: err.reason });
|
|
68
|
+
* return;
|
|
69
|
+
* }
|
|
70
|
+
* next(err);
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* // Override: consumer fully owns the response
|
|
74
|
+
* app.get("/orgs/:id", idParam("id", org, {
|
|
75
|
+
* onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),
|
|
76
|
+
* }), handler);
|
|
77
|
+
*
|
|
78
|
+
* // Or a lightweight status remap without a full handler
|
|
79
|
+
* app.get("/things/:id", idParam("id", thing, { status: { brand_mismatch: 400 } }), handler);
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
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;
|
|
83
|
+
//#endregion
|
|
84
|
+
export { IdParamError, type IdParamFailure, IdParamOptions, idParam };
|
|
85
|
+
//# sourceMappingURL=express.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.mts","names":[],"sources":["../src/express.ts"],"mappings":";;;;;KAMK,OAAA;EACH,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;cAO5B,YAAA,SAAqB,KAAA;EAAA,SACvB,MAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,MAAA;AAAA;AAJtD;AAAA,KAaY,cAAA;;;;;EAKV,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,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
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//#region src/express.ts
|
|
2
|
+
/**
|
|
3
|
+
* Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.
|
|
4
|
+
* Inspect `err.reason` and `err.status` in error-handling middleware.
|
|
5
|
+
*/
|
|
6
|
+
var IdParamError = class extends Error {
|
|
7
|
+
status;
|
|
8
|
+
reason;
|
|
9
|
+
constructor(reason, status) {
|
|
10
|
+
super(`ID validation failed: ${reason}`);
|
|
11
|
+
this.name = "IdParamError";
|
|
12
|
+
this.reason = reason;
|
|
13
|
+
this.status = status;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Express middleware that validates a named route param against a codec via `safeParse`.
|
|
18
|
+
*
|
|
19
|
+
* **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and `reason`,
|
|
20
|
+
* so the app's existing error-handling middleware controls rendering. The adapter does not write
|
|
21
|
+
* a response body itself.
|
|
22
|
+
*
|
|
23
|
+
* **`options.onError`:** when provided, the hook owns the response entirely — the adapter does
|
|
24
|
+
* not call `next(err)`.
|
|
25
|
+
*
|
|
26
|
+
* **`options.status`:** remaps the default HTTP status for a reason without a full handler.
|
|
27
|
+
*
|
|
28
|
+
* - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
|
|
29
|
+
* - **Malformed or missing ID → `reason: "malformed"`, default 400**
|
|
30
|
+
*
|
|
31
|
+
* On success, stores the canonical `Id<Brand>` in `res.locals` under `paramName`
|
|
32
|
+
* and calls `next()`.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { idParam, IdParamError } from "@smonn/ids/express";
|
|
37
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
38
|
+
*
|
|
39
|
+
* const usr = createTimestampId("usr");
|
|
40
|
+
*
|
|
41
|
+
* // Default: forwards error to app error-handling middleware
|
|
42
|
+
* app.get("/users/:id", idParam("id", usr), (req, res) => {
|
|
43
|
+
* const id = res.locals.id; // Id<"usr">, canonical
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* // Error-handling middleware receives the typed error
|
|
47
|
+
* app.use((err, req, res, next) => {
|
|
48
|
+
* if (err instanceof IdParamError) {
|
|
49
|
+
* res.status(err.status).json({ error: err.reason });
|
|
50
|
+
* return;
|
|
51
|
+
* }
|
|
52
|
+
* next(err);
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* // Override: consumer fully owns the response
|
|
56
|
+
* app.get("/orgs/:id", idParam("id", org, {
|
|
57
|
+
* onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),
|
|
58
|
+
* }), handler);
|
|
59
|
+
*
|
|
60
|
+
* // Or a lightweight status remap without a full handler
|
|
61
|
+
* app.get("/things/:id", idParam("id", thing, { status: { brand_mismatch: 400 } }), handler);
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
function idParam(paramName, codec, options) {
|
|
65
|
+
return (req, res, next) => {
|
|
66
|
+
const raw = req.params[paramName];
|
|
67
|
+
const result = codec.safeParse(raw);
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
const reason = result.error === "invalid_prefix" ? "brand_mismatch" : "malformed";
|
|
70
|
+
const defaultStatus = reason === "brand_mismatch" ? 404 : 400;
|
|
71
|
+
const status = options?.status?.[reason] ?? defaultStatus;
|
|
72
|
+
const failure = {
|
|
73
|
+
reason,
|
|
74
|
+
status
|
|
75
|
+
};
|
|
76
|
+
if (options?.onError) {
|
|
77
|
+
options.onError(failure, req, res, next);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
next(new IdParamError(reason, status));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
res.locals[paramName] = result.id;
|
|
84
|
+
next();
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { IdParamError, idParam };
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=express.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.mjs","names":[],"sources":["../src/express.ts"],"sourcesContent":["import type { NextFunction, Request, Response } from \"express\";\nimport type { IdParamFailure } from \"./adapter-types.js\";\nimport type { Id, ParseResult } from \"./types.js\";\n\nexport type { IdParamFailure };\n\ntype IdCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\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 reason =\n result.error === \"invalid_prefix\" ? (\"brand_mismatch\" as const) : (\"malformed\" as const);\n const defaultStatus = reason === \"brand_mismatch\" ? 404 : 400;\n const status = options?.status?.[reason] ?? defaultStatus;\n const failure: IdParamFailure = { reason, status };\n if (options?.onError) {\n options.onError(failure, req, res, next);\n return;\n }\n next(new IdParamError(reason, status));\n return;\n }\n (res.locals as Record<string, unknown>)[paramName] = result.id;\n next();\n };\n}\n"],"mappings":";;;;;AAcA,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,SACJ,OAAO,UAAU,mBAAoB,mBAA8B;GACrE,MAAM,gBAAgB,WAAW,mBAAmB,MAAM;GAC1D,MAAM,SAAS,SAAS,SAAS,WAAW;GAC5C,MAAM,UAA0B;IAAE;IAAQ;GAAO;GACjD,IAAI,SAAS,SAAS;IACpB,QAAQ,QAAQ,SAAS,KAAK,KAAK,IAAI;IACvC;GACF;GACA,KAAK,IAAI,aAAa,QAAQ,MAAM,CAAC;GACrC;EACF;EACA,IAAK,OAAmC,aAAa,OAAO;EAC5D,KAAK;CACP;AACF"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { i as ParseResult } from "./types-g7CiQDyE.mjs";
|
|
2
|
+
import { t as IdParamFailure } from "./adapter-types-oHCCSgOO.mjs";
|
|
3
|
+
import { FastifyReply, FastifyRequest } from "fastify";
|
|
4
|
+
|
|
5
|
+
//#region src/fastify.d.ts
|
|
6
|
+
type IdCodec<Brand extends string> = {
|
|
7
|
+
safeParse(value: unknown): ParseResult<Brand>;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Typed error thrown into Fastify's `setErrorHandler` on validation failure.
|
|
11
|
+
* Inspect `err.reason` and `err.statusCode` in your error handler.
|
|
12
|
+
*/
|
|
13
|
+
declare class IdParamError extends Error {
|
|
14
|
+
readonly statusCode: number;
|
|
15
|
+
readonly reason: "brand_mismatch" | "malformed";
|
|
16
|
+
constructor(reason: "brand_mismatch" | "malformed", statusCode: number);
|
|
17
|
+
}
|
|
18
|
+
/** Options for `idParam`. All fields are optional. */
|
|
19
|
+
type IdParamOptions = {
|
|
20
|
+
/**
|
|
21
|
+
* Called instead of throwing when provided. The hook owns the response entirely —
|
|
22
|
+
* the adapter does not throw.
|
|
23
|
+
*/
|
|
24
|
+
onError?: (failure: IdParamFailure, request: FastifyRequest, reply: FastifyReply) => void | Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Remap the default HTTP status for a failure reason without a full handler.
|
|
27
|
+
* e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.
|
|
28
|
+
*/
|
|
29
|
+
status?: {
|
|
30
|
+
brand_mismatch?: number;
|
|
31
|
+
malformed?: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Fastify `preHandler` hook factory that validates a named route param against a codec via `safeParse`.
|
|
36
|
+
*
|
|
37
|
+
* **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the app's
|
|
38
|
+
* existing `setErrorHandler` controls rendering. The adapter does not write a response body itself.
|
|
39
|
+
*
|
|
40
|
+
* **`options.onError`:** when provided, the hook calls `onError` and does not throw; the consumer
|
|
41
|
+
* fully owns the response via `reply`.
|
|
42
|
+
*
|
|
43
|
+
* **`options.status`:** remaps the default HTTP status for a reason without a full handler.
|
|
44
|
+
*
|
|
45
|
+
* - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
|
|
46
|
+
* - **Malformed or missing ID → `reason: "malformed"`, default 400**
|
|
47
|
+
*
|
|
48
|
+
* On success, stores the canonical `Id<Brand>` in `request.params` under `paramName`.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { idParam, IdParamError } from "@smonn/ids/fastify";
|
|
53
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
54
|
+
*
|
|
55
|
+
* const usr = createTimestampId("usr");
|
|
56
|
+
*
|
|
57
|
+
* // Default: throws IdParamError → setErrorHandler renders it
|
|
58
|
+
* fastify.get("/users/:id", { preHandler: idParam("id", usr) }, (request, reply) => {
|
|
59
|
+
* const id = request.params.id; // Id<"usr">, canonical
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* // Error handler receives the typed error
|
|
63
|
+
* fastify.setErrorHandler((err, request, reply) => {
|
|
64
|
+
* if (err instanceof IdParamError) {
|
|
65
|
+
* reply.status(err.statusCode).send({ error: err.reason });
|
|
66
|
+
* return;
|
|
67
|
+
* }
|
|
68
|
+
* reply.send(err);
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // Override: consumer fully owns the error response
|
|
72
|
+
* fastify.get("/orgs/:id", {
|
|
73
|
+
* preHandler: idParam("id", org, {
|
|
74
|
+
* onError: (failure, request, reply) =>
|
|
75
|
+
* reply.status(failure.status).send({ error: failure.reason }),
|
|
76
|
+
* }),
|
|
77
|
+
* }, handler);
|
|
78
|
+
*
|
|
79
|
+
* // Or a lightweight status remap without a full handler
|
|
80
|
+
* fastify.get("/things/:id", {
|
|
81
|
+
* preHandler: idParam("id", thing, { status: { brand_mismatch: 400 } }),
|
|
82
|
+
* }, handler);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function idParam<ParamKey extends string, Brand extends string>(paramName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
86
|
+
//#endregion
|
|
87
|
+
export { IdParamError, type IdParamFailure, IdParamOptions, idParam };
|
|
88
|
+
//# sourceMappingURL=fastify.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../src/fastify.ts"],"mappings":";;;;;KAMK,OAAA;EACH,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;cAO5B,YAAA,SAAqB,KAAA;EAAA,SACvB,UAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,UAAA;AAAA;AAJtD;AAAA,KAaY,cAAA;;;;;EAKV,OAAA,IACE,OAAA,EAAS,cAAA,EACT,OAAA,EAAS,cAAA,EACT,KAAA,EAAO,YAAA,YACG,OAAA;;;;;EAKZ,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;;;;;;;;AAAA;AAsDtC;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIqD;;;;;;iBAJrC,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IACR,OAAA,EAAS,cAAA,EAAgB,KAAA,EAAO,YAAA,KAAiB,OAAA"}
|