@nxgt/graphql-validation 0.2.1 → 0.3.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 (59) hide show
  1. package/README.md +62 -5
  2. package/dist/builder/check-constraints.d.ts +6 -1
  3. package/dist/builder/check-constraints.d.ts.map +1 -1
  4. package/dist/builder/input-code.d.ts +14 -1
  5. package/dist/builder/input-code.d.ts.map +1 -1
  6. package/dist/builder/input-schema.d.ts +3 -1
  7. package/dist/builder/input-schema.d.ts.map +1 -1
  8. package/dist/builder/leaf.d.ts +2 -2
  9. package/dist/builder/leaf.d.ts.map +1 -1
  10. package/dist/chunks/{index-s1kkmg35.js → index-1mzzydy3.js} +219 -23
  11. package/dist/chunks/index-1mzzydy3.js.map +25 -0
  12. package/dist/chunks/{index-rbxe5m3y.js → index-xcaf9kes.js} +24 -97
  13. package/dist/chunks/index-xcaf9kes.js.map +27 -0
  14. package/dist/cli.js +1 -1
  15. package/dist/codegen.d.ts +1 -0
  16. package/dist/codegen.d.ts.map +1 -1
  17. package/dist/codegen.js +19 -9
  18. package/dist/codegen.js.map +3 -3
  19. package/dist/formats/byte.d.ts +1 -1
  20. package/dist/formats/byte.d.ts.map +1 -1
  21. package/dist/formats/date-time.d.ts +1 -1
  22. package/dist/formats/date-time.d.ts.map +1 -1
  23. package/dist/formats/date.d.ts +1 -1
  24. package/dist/formats/date.d.ts.map +1 -1
  25. package/dist/formats/email.d.ts +1 -1
  26. package/dist/formats/email.d.ts.map +1 -1
  27. package/dist/formats/format.d.ts +9 -3
  28. package/dist/formats/format.d.ts.map +1 -1
  29. package/dist/formats/index.d.ts +6 -8
  30. package/dist/formats/index.d.ts.map +1 -1
  31. package/dist/formats/ipv4.d.ts +1 -1
  32. package/dist/formats/ipv4.d.ts.map +1 -1
  33. package/dist/formats/ipv6.d.ts +1 -1
  34. package/dist/formats/ipv6.d.ts.map +1 -1
  35. package/dist/formats/registry.d.ts +50 -0
  36. package/dist/formats/registry.d.ts.map +1 -0
  37. package/dist/formats/uri.d.ts +1 -1
  38. package/dist/formats/uri.d.ts.map +1 -1
  39. package/dist/formats/uuid.d.ts +1 -1
  40. package/dist/formats/uuid.d.ts.map +1 -1
  41. package/dist/index.d.ts +4 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +18 -5
  44. package/dist/index.js.map +3 -3
  45. package/dist/rules/format.d.ts.map +1 -1
  46. package/dist/rules/index.d.ts +8 -3
  47. package/dist/rules/index.d.ts.map +1 -1
  48. package/dist/rules/rule.d.ts +19 -2
  49. package/dist/rules/rule.d.ts.map +1 -1
  50. package/dist/with-validation.d.ts +17 -1
  51. package/dist/with-validation.d.ts.map +1 -1
  52. package/docs/README.md +1 -1
  53. package/docs/guide/constraints.md +133 -4
  54. package/docs/guide/errors.md +4 -1
  55. package/docs/roadmap.md +19 -15
  56. package/docs/troubleshooting.md +151 -4
  57. package/package.json +2 -2
  58. package/dist/chunks/index-rbxe5m3y.js.map +0 -37
  59. package/dist/chunks/index-s1kkmg35.js.map +0 -14
@@ -0,0 +1,50 @@
1
+ import type { Format, StringSchema } from './format.js';
2
+ import { type FormatName } from './index.js';
3
+ /**
4
+ * The application's own formats, keyed by the name `@constraint(format:
5
+ * "...")` gives them: each a Zod string schema, `z.string()` or a string
6
+ * format (`z.email()`), narrowed (`.regex()`, `.refine()`) but never
7
+ * transformed nor rewritten (`.trim()`, `.toLowerCase()`, `z.url()`,
8
+ * `z.coerce.string()`), so the resolver receives what was sent.
9
+ */
10
+ export type FormatSchemas = {
11
+ readonly [name: string]: StringSchema;
12
+ };
13
+ /**
14
+ * `F` with every key that names a built-in format typed `never`, so
15
+ * replacing one is a type error before it is a startup error. The keys are
16
+ * optional: a record typed wider than its literal (`FormatSchemas`,
17
+ * `Record<string, z.ZodString>`) may hold none of them, and passes.
18
+ */
19
+ export type OwnFormats<F extends FormatSchemas> = F & {
20
+ readonly [K in keyof F & FormatName]?: never;
21
+ };
22
+ /**
23
+ * Every format `@constraint(format: "...")` can name: the built-in ones,
24
+ * then the application's own. A lookup is one `Map` read, however many
25
+ * formats there are.
26
+ */
27
+ export declare class FormatRegistry {
28
+ #private;
29
+ /**
30
+ * Fails, naming the format, on a bad name, a built-in's name, a schema
31
+ * that is not a string's, one zod cannot run, or one that rewrites the
32
+ * value.
33
+ */
34
+ constructor(own?: FormatSchemas);
35
+ /** The format named `name`, or an error that lists the known ones. */
36
+ named(name: string): Format;
37
+ /**
38
+ * The format of the application that last went async, then forgotten: read
39
+ * right after a synchronous parse threw zod's `$ZodAsyncError`, it names the
40
+ * format that made it throw (zod stops at the first check that returns a
41
+ * promise, and only an application's format can).
42
+ */
43
+ takeAsync(): string | undefined;
44
+ }
45
+ /**
46
+ * The registry of a set of formats, built (and checked) once per record: a
47
+ * code generator hands the same record for every field.
48
+ */
49
+ export declare function registryOf(own?: FormatSchemas): FormatRegistry;
50
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/formats/registry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAAA;CAAE,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,aAAa,IAAI,CAAC,GAAG;IACrD,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,KAAK;CAC5C,CAAC;AAIF;;;;GAIG;AACH,qBAAa,cAAc;;IAI1B;;;;OAIG;gBACS,GAAG,GAAE,aAAkB;IAoCnC,sEAAsE;IACtE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAQ3B;;;;;OAKG;IACH,SAAS,IAAI,MAAM,GAAG,SAAS;CAK/B;AA4HD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,aAAa,GAAG,cAAc,CAQ9D"}
@@ -1,2 +1,2 @@
1
- export declare const uriFormat: import("./format.js").Format<"uri">;
1
+ export declare const uriFormat: import("./format.js").BuiltInFormat<"uri">;
2
2
  //# sourceMappingURL=uri.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"uri.d.ts","sourceRoot":"","sources":["../../src/formats/uri.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,SAAS,kCAIpB,CAAC"}
1
+ {"version":3,"file":"uri.d.ts","sourceRoot":"","sources":["../../src/formats/uri.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,SAAS,yCAIpB,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const uuidFormat: import("./format.js").Format<"uuid">;
1
+ export declare const uuidFormat: import("./format.js").BuiltInFormat<"uuid">;
2
2
  //# sourceMappingURL=uuid.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../../src/formats/uuid.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,UAAU,mCAIrB,CAAC"}
1
+ {"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../../src/formats/uuid.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,UAAU,0CAIrB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { type BadUserInputExtensions, badUserInput, type ValidationIssue, } from './bad-user-input.js';
2
2
  export { constraintTypeDefs } from './constraint-directive.js';
3
+ export type { FormatName } from './formats/index.js';
4
+ export type { StringSchema } from './formats/format.js';
5
+ export type { FormatSchemas, OwnFormats } from './formats/registry.js';
3
6
  export type { ConstraintArgument } from './rules/index.js';
4
7
  export { type ArgsSchema, type SchemaOf, validated } from './validated.js';
5
- export { withValidation } from './with-validation.js';
8
+ export { type WithValidationOptions, withValidation, } from './with-validation.js';
6
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,sBAAsB,EAC3B,YAAY,EACZ,KAAK,eAAe,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,sBAAsB,EAC3B,YAAY,EACZ,KAAK,eAAe,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACpE,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EACN,KAAK,qBAAqB,EAC1B,cAAc,GACd,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  constraintOf,
3
3
  constraintTypeDefs2
4
- } from "./chunks/index-rbxe5m3y.js";
4
+ } from "./chunks/index-xcaf9kes.js";
5
5
  import {
6
6
  checkConstraints2
7
- } from "./chunks/index-s1kkmg35.js";
7
+ } from "./chunks/index-1mzzydy3.js";
8
8
 
9
9
  // src/bad-user-input.ts
10
10
  import { GraphQLError } from "graphql";
@@ -47,6 +47,7 @@ import {
47
47
  defaultFieldResolver
48
48
  } from "graphql";
49
49
  var wrapped = Symbol.for("@nxgt/graphql-validation/wrapped");
50
+ var wrappedWith = Symbol.for("@nxgt/graphql-validation/formats");
50
51
  function checking(schema, where, resolve) {
51
52
  const check = async (source, args, context, info) => await resolve(source, await parseArgs(schema, args, where, constraintOf), context, info);
52
53
  check[wrapped] = true;
@@ -63,16 +64,28 @@ function wrap(field, args, where, subscription) {
63
64
  if (!resolve[wrapped])
64
65
  field.resolve = checking(args, where, resolve);
65
66
  }
66
- function withValidation(schema) {
67
+ function withValidation(schema, options = {}) {
68
+ const formats = options.formats ?? {};
69
+ const holder = schema;
70
+ const before = holder[wrappedWith];
71
+ if (before && !sameFormats(before, formats)) {
72
+ throw new Error("withValidation: this schema is already wrapped with other formats. Call withValidation once, with every format.");
73
+ }
67
74
  const directive = checkConstraints2(schema, (type, field, args) => {
68
75
  const subscription = type === schema.getSubscriptionType();
69
76
  wrap(field, args, `${type.name}.${field.name}`, subscription);
70
- });
77
+ }, formats);
71
78
  if (!directive) {
72
79
  throw new Error("withValidation: this schema declares no @constraint directive. Add constraintTypeDefs to its type definitions.");
73
80
  }
81
+ if (!before)
82
+ Object.defineProperty(schema, wrappedWith, { value: formats });
74
83
  return schema;
75
84
  }
85
+ function sameFormats(a, b) {
86
+ const names = Object.keys(a);
87
+ return names.length === Object.keys(b).length && names.every((name) => Object.hasOwn(b, name) && a[name] === b[name]);
88
+ }
76
89
  export {
77
90
  badUserInput,
78
91
  constraintTypeDefs2 as constraintTypeDefs,
@@ -80,5 +93,5 @@ export {
80
93
  withValidation
81
94
  };
82
95
 
83
- //# debugId=EAADEEA4CBC8F7A764756E2164756E21
96
+ //# debugId=B588BD3EF63841CA64756E2164756E21
84
97
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -4,9 +4,9 @@
4
4
  "sourcesContent": [
5
5
  "import { GraphQLError } from 'graphql';\nimport type { z } from 'zod';\nimport type { ConstraintArgument } from './rules';\n\n/** One reason an input was refused, at its path inside the arguments. */\nexport interface ValidationIssue {\n\t/** `['input', 'address', 'zip']`: the argument, then the input fields. */\n\treadonly path: readonly (string | number)[];\n\treadonly message: string;\n\t/** Zod's issue code: `too_small`, `invalid_format`, `custom`, ... */\n\treadonly code: string;\n\t/**\n\t * The `@constraint` argument that refused (`minLength`, `format`), when a\n\t * directive did: stable across Zod versions, unlike `code`. Absent for a\n\t * refusal of `validated()`'s own schema.\n\t */\n\treadonly constraint?: ConstraintArgument;\n}\n\n/** The `extensions` of the error an invalid input becomes. */\nexport type BadUserInputExtensions = {\n\treadonly code: 'BAD_USER_INPUT';\n\treadonly issues: readonly ValidationIssue[];\n};\n\n/** Names the `@constraint` argument behind an issue, when there is one. */\nexport type ConstraintOf = (\n\tissue: z.core.$ZodIssue,\n) => ConstraintArgument | undefined;\n\nfunction issuesOf(\n\terror: z.ZodError,\n\tconstraintOf?: ConstraintOf,\n): ValidationIssue[] {\n\treturn error.issues.map((issue) => {\n\t\tconst path = issue.path.map((key) =>\n\t\t\ttypeof key === 'number' ? key : String(key),\n\t\t);\n\t\tconst constraint = constraintOf?.(issue);\n\t\treturn constraint\n\t\t\t? { path, message: issue.message, code: issue.code, constraint }\n\t\t\t: { path, message: issue.message, code: issue.code };\n\t});\n}\n\nfunction errorOf(where: string, issues: ValidationIssue[]): GraphQLError {\n\tconst [first] = issues;\n\tconst reason = first\n\t\t? `${first.path.join('.') || 'arguments'}: ${first.message}`\n\t\t: 'refused';\n\tconst extensions: BadUserInputExtensions = { code: 'BAD_USER_INPUT', issues };\n\treturn new GraphQLError(`Invalid arguments for ${where}. ${reason}`, {\n\t\textensions,\n\t});\n}\n\n/**\n * The one error an invalid input becomes: `extensions.code` is\n * `BAD_USER_INPUT`, the code Apollo and most clients already read, and\n * `extensions.issues` lists every reason with its path, so a form can show\n * each one beside its field. The message names the field and the first\n * reason, for a log line.\n */\nexport function badUserInput(where: string, error: z.ZodError): GraphQLError {\n\treturn errorOf(where, issuesOf(error));\n}\n\n/** The parsed arguments, or the error above, its issues named by `constraintOf`. */\nexport async function parseArgs(\n\tschema: z.ZodType,\n\targs: unknown,\n\twhere: string,\n\tconstraintOf?: ConstraintOf,\n): Promise<unknown> {\n\tconst result = await schema.safeParseAsync(args);\n\tif (!result.success)\n\t\tthrow errorOf(where, issuesOf(result.error, constraintOf));\n\treturn result.data;\n}\n",
6
6
  "import type { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql';\nimport { z } from 'zod';\nimport { parseArgs } from './bad-user-input';\n\n/** An object schema of the arguments, or its shape. */\nexport type ArgsSchema = z.ZodType<Record<string, unknown>> | z.ZodRawShape;\n\n/** The schema `ArgsSchema` stands for. */\nexport type SchemaOf<S extends ArgsSchema> = S extends z.ZodType\n\t? S\n\t: z.ZodObject<S & z.ZodRawShape>;\n\n/**\n * A resolver whose arguments are checked by `schema` before it runs, for what\n * a `@constraint` cannot say: a rule across arguments, a business refinement,\n * an async check. It receives the parsed arguments (`z.output`), defaults and\n * transforms applied; an invalid input is the same `BAD_USER_INPUT` error as\n * the directives'.\n *\n * ```ts\n * signUp: validated(\n * z.object({ input: signUpSchema }).refine(({ input }) => input.password !== input.email),\n * (_, { input }) => users.create(input),\n * )\n * ```\n */\nexport function validated<\n\tS extends ArgsSchema,\n\tTSource = unknown,\n\tTContext = unknown,\n\tTResult = unknown,\n>(\n\tschema: S,\n\tresolve: (\n\t\tsource: TSource,\n\t\targs: z.output<SchemaOf<S>>,\n\t\tcontext: TContext,\n\t\tinfo: GraphQLResolveInfo,\n\t) => TResult,\n): GraphQLFieldResolver<\n\tTSource,\n\tTContext,\n\tz.input<SchemaOf<S>>,\n\tPromise<Awaited<TResult>>\n> {\n\t// `_zod`, not `instanceof`: a schema from another copy of zod is still one.\n\tconst checked = (\n\t\t'_zod' in schema ? schema : z.object(schema as z.ZodRawShape)\n\t) as z.ZodType;\n\treturn async (source, args, context, info): Promise<Awaited<TResult>> => {\n\t\tconst where = `${info.parentType.name}.${info.fieldName}`;\n\t\tconst parsed = (await parseArgs(checked, args, where)) as z.output<\n\t\t\tSchemaOf<S>\n\t\t>;\n\t\treturn await resolve(source, parsed, context, info);\n\t};\n}\n",
7
- "import {\n\tdefaultFieldResolver,\n\ttype GraphQLField,\n\ttype GraphQLFieldResolver,\n\ttype GraphQLSchema,\n} from 'graphql';\nimport type { z } from 'zod';\nimport { parseArgs } from './bad-user-input';\nimport { checkConstraints } from './builder/check-constraints';\nimport { constraintOf } from './rules';\n\n// Symbol.for: two copies of the package in one tree still wrap a field once.\nconst wrapped = Symbol.for('@nxgt/graphql-validation/wrapped');\n\ntype Resolver = GraphQLFieldResolver<unknown, unknown> & { [wrapped]?: true };\n\nfunction checking(\n\tschema: z.ZodType,\n\twhere: string,\n\tresolve: Resolver,\n): Resolver {\n\tconst check: Resolver = async (source, args, context, info) =>\n\t\tawait resolve(\n\t\t\tsource,\n\t\t\tawait parseArgs(schema, args, where, constraintOf),\n\t\t\tcontext,\n\t\t\tinfo,\n\t\t);\n\tcheck[wrapped] = true;\n\treturn check;\n}\n\n/**\n * Wraps a field once. A subscription field is checked in `subscribe`, which\n * runs once per subscription — graphql's default one when the field has none\n * — and its `resolve` runs once per event, with the same arguments, already\n * checked.\n */\nfunction wrap(\n\tfield: GraphQLField<unknown, unknown>,\n\targs: z.ZodType,\n\twhere: string,\n\tsubscription: boolean,\n): void {\n\tconst subscribe = (field.subscribe ??\n\t\t(subscription ? defaultFieldResolver : undefined)) as Resolver | undefined;\n\tif (subscribe) {\n\t\tif (!subscribe[wrapped]) field.subscribe = checking(args, where, subscribe);\n\t\treturn;\n\t}\n\tconst resolve = (field.resolve ?? defaultFieldResolver) as Resolver;\n\tif (!resolve[wrapped]) field.resolve = checking(args, where, resolve);\n}\n\n/**\n * Checks every `@constraint` of `schema` before the resolvers run. Each\n * field with a constrained argument — directly, or anywhere inside an input\n * type it takes — gets its resolver (and its `subscribe`) wrapped: the\n * arguments are parsed by the Zod schema the directives describe, the\n * resolver receives the parsed value, and an invalid input becomes one\n * `BAD_USER_INPUT` error. Fields with nothing to check are left untouched.\n *\n * The schema is built here, once: a constraint that cannot apply (a\n * `minLength` on an `Int`, an unknown format, a bad pattern) throws now, not\n * on the first request. The resolvers are wrapped in place and the same\n * schema is returned, so it goes wherever it went before; calling it twice\n * wraps nothing twice. Call it last, once every resolver is attached: a\n * resolver set on a field afterwards replaces the check.\n *\n * A `@constraint` on an interface's argument must be repeated on each\n * object's field, which is the one that resolves; a field that drops it\n * fails here.\n *\n * The directives are read from the SDL (`astNode`), so a schema built from\n * type definitions — `buildSchema`, `makeExecutableSchema`, Yoga, Apollo —\n * is checked; a code-first schema with no SDL has no `@constraint` to read.\n */\nexport function withValidation<S extends GraphQLSchema>(schema: S): S {\n\tconst directive = checkConstraints(schema, (type, field, args) => {\n\t\tconst subscription = type === schema.getSubscriptionType();\n\t\twrap(field, args, `${type.name}.${field.name}`, subscription);\n\t});\n\tif (!directive) {\n\t\tthrow new Error(\n\t\t\t'withValidation: this schema declares no @constraint directive. Add constraintTypeDefs to its type definitions.',\n\t\t);\n\t}\n\treturn schema;\n}\n"
7
+ "import {\n\tdefaultFieldResolver,\n\ttype GraphQLField,\n\ttype GraphQLFieldResolver,\n\ttype GraphQLSchema,\n} from 'graphql';\nimport type { z } from 'zod';\nimport { parseArgs } from './bad-user-input';\nimport { checkConstraints } from './builder/check-constraints';\nimport type { FormatSchemas, OwnFormats } from './formats/registry';\nimport { constraintOf } from './rules';\n\n// Symbol.for: two copies of the package in one tree still wrap a field once.\nconst wrapped = Symbol.for('@nxgt/graphql-validation/wrapped');\n// The formats a schema was wrapped with, kept on the schema itself.\nconst wrappedWith = Symbol.for('@nxgt/graphql-validation/formats');\n\n/** What `withValidation` takes besides the schema. */\nexport interface WithValidationOptions<\n\tF extends FormatSchemas = FormatSchemas,\n> {\n\t/**\n\t * The application's own formats, which `@constraint(format: \"...\")` may\n\t * name beside the built-in ones: one Zod string schema each, keyed by its\n\t * name (lowercase letters, digits and hyphens). A built-in name is a type\n\t * error, and refused at startup.\n\t */\n\treadonly formats?: OwnFormats<F>;\n}\n\ntype Resolver = GraphQLFieldResolver<unknown, unknown> & { [wrapped]?: true };\n\nfunction checking(\n\tschema: z.ZodType,\n\twhere: string,\n\tresolve: Resolver,\n): Resolver {\n\tconst check: Resolver = async (source, args, context, info) =>\n\t\tawait resolve(\n\t\t\tsource,\n\t\t\tawait parseArgs(schema, args, where, constraintOf),\n\t\t\tcontext,\n\t\t\tinfo,\n\t\t);\n\tcheck[wrapped] = true;\n\treturn check;\n}\n\n/**\n * Wraps a field once. A subscription field is checked in `subscribe`, which\n * runs once per subscription — graphql's default one when the field has none\n * — and its `resolve` runs once per event, with the same arguments, already\n * checked.\n */\nfunction wrap(\n\tfield: GraphQLField<unknown, unknown>,\n\targs: z.ZodType,\n\twhere: string,\n\tsubscription: boolean,\n): void {\n\tconst subscribe = (field.subscribe ??\n\t\t(subscription ? defaultFieldResolver : undefined)) as Resolver | undefined;\n\tif (subscribe) {\n\t\tif (!subscribe[wrapped]) field.subscribe = checking(args, where, subscribe);\n\t\treturn;\n\t}\n\tconst resolve = (field.resolve ?? defaultFieldResolver) as Resolver;\n\tif (!resolve[wrapped]) field.resolve = checking(args, where, resolve);\n}\n\n/**\n * Checks every `@constraint` of `schema` before the resolvers run. Each\n * field with a constrained argument — directly, or anywhere inside an input\n * type it takes — gets its resolver (and its `subscribe`) wrapped: the\n * arguments are parsed by the Zod schema the directives describe, the\n * resolver receives the parsed value, and an invalid input becomes one\n * `BAD_USER_INPUT` error. Fields with nothing to check are left untouched.\n *\n * The schema is built here, once: a constraint that cannot apply (a\n * `minLength` on an `Int`, an unknown format, a bad pattern) throws now, not\n * on the first request. The resolvers are wrapped in place and the same\n * schema is returned, so it goes wherever it went before; calling it twice\n * wraps nothing twice. Call it last, once every resolver is attached: a\n * resolver set on a field afterwards replaces the check.\n *\n * A `@constraint` on an interface's argument must be repeated on each\n * object's field, which is the one that resolves; a field that drops it\n * fails here.\n *\n * The directives are read from the SDL (`astNode`), so a schema built from\n * type definitions — `buildSchema`, `makeExecutableSchema`, Yoga, Apollo —\n * is checked; a code-first schema with no SDL has no `@constraint` to read.\n *\n * `options.formats` adds the application's own formats; wrapping a schema\n * again throws unless the record has the same names, each the very same\n * schema (no formats after some, or some after none, throws too), since its\n * fields are already checked against the first ones.\n */\nexport function withValidation<\n\tS extends GraphQLSchema,\n\t// biome-ignore lint/complexity/noBannedTypes: no formats of its own by default\n\tconst F extends FormatSchemas = {},\n>(schema: S, options: WithValidationOptions<F> = {}): S {\n\tconst formats: FormatSchemas = options.formats ?? {};\n\tconst holder = schema as unknown as { [wrappedWith]?: FormatSchemas };\n\tconst before = holder[wrappedWith];\n\tif (before && !sameFormats(before, formats)) {\n\t\tthrow new Error(\n\t\t\t'withValidation: this schema is already wrapped with other formats. Call withValidation once, with every format.',\n\t\t);\n\t}\n\tconst directive = checkConstraints(\n\t\tschema,\n\t\t(type, field, args) => {\n\t\t\tconst subscription = type === schema.getSubscriptionType();\n\t\t\twrap(field, args, `${type.name}.${field.name}`, subscription);\n\t\t},\n\t\tformats,\n\t);\n\tif (!directive) {\n\t\tthrow new Error(\n\t\t\t'withValidation: this schema declares no @constraint directive. Add constraintTypeDefs to its type definitions.',\n\t\t);\n\t}\n\tif (!before) Object.defineProperty(schema, wrappedWith, { value: formats });\n\treturn schema;\n}\n\n/** The same names, each the very same schema. */\nfunction sameFormats(a: FormatSchemas, b: FormatSchemas): boolean {\n\tconst names = Object.keys(a);\n\treturn (\n\t\tnames.length === Object.keys(b).length &&\n\t\tnames.every((name) => Object.hasOwn(b, name) && a[name] === b[name])\n\t);\n}\n"
8
8
  ],
9
- "mappings": ";;;;;;;;;AAAA;AA8BA,SAAS,QAAQ,CAChB,OACA,cACoB;AAAA,EACpB,OAAO,MAAM,OAAO,IAAI,CAAC,UAAU;AAAA,IAClC,MAAM,OAAO,MAAM,KAAK,IAAI,CAAC,QAC5B,OAAO,QAAQ,WAAW,MAAM,OAAO,GAAG,CAC3C;AAAA,IACA,MAAM,aAAa,eAAe,KAAK;AAAA,IACvC,OAAO,aACJ,EAAE,MAAM,SAAS,MAAM,SAAS,MAAM,MAAM,MAAM,WAAW,IAC7D,EAAE,MAAM,SAAS,MAAM,SAAS,MAAM,MAAM,KAAK;AAAA,GACpD;AAAA;AAGF,SAAS,OAAO,CAAC,OAAe,QAAyC;AAAA,EACxE,OAAO,SAAS;AAAA,EAChB,MAAM,SAAS,QACZ,GAAG,MAAM,KAAK,KAAK,GAAG,KAAK,gBAAgB,MAAM,YACjD;AAAA,EACH,MAAM,aAAqC,EAAE,MAAM,kBAAkB,OAAO;AAAA,EAC5E,OAAO,IAAI,aAAa,yBAAyB,UAAU,UAAU;AAAA,IACpE;AAAA,EACD,CAAC;AAAA;AAUK,SAAS,YAAY,CAAC,OAAe,OAAiC;AAAA,EAC5E,OAAO,QAAQ,OAAO,SAAS,KAAK,CAAC;AAAA;AAItC,eAAsB,SAAS,CAC9B,QACA,MACA,OACA,cACmB;AAAA,EACnB,MAAM,SAAS,MAAM,OAAO,eAAe,IAAI;AAAA,EAC/C,IAAI,CAAC,OAAO;AAAA,IACX,MAAM,QAAQ,OAAO,SAAS,OAAO,OAAO,YAAY,CAAC;AAAA,EAC1D,OAAO,OAAO;AAAA;;AC5Ef;AAyBO,SAAS,SAKf,CACA,QACA,SAWC;AAAA,EAED,MAAM,UACL,UAAU,SAAS,SAAS,EAAE,OAAO,MAAuB;AAAA,EAE7D,OAAO,OAAO,QAAQ,MAAM,SAAS,SAAoC;AAAA,IACxE,MAAM,QAAQ,GAAG,KAAK,WAAW,QAAQ,KAAK;AAAA,IAC9C,MAAM,SAAU,MAAM,UAAU,SAAS,MAAM,KAAK;AAAA,IAGpD,OAAO,MAAM,QAAQ,QAAQ,QAAQ,SAAS,IAAI;AAAA;AAAA;;ACtDpD;AAAA;AAAA;AAYA,IAAM,UAAU,OAAO,IAAI,kCAAkC;AAI7D,SAAS,QAAQ,CAChB,QACA,OACA,SACW;AAAA,EACX,MAAM,QAAkB,OAAO,QAAQ,MAAM,SAAS,SACrD,MAAM,QACL,QACA,MAAM,UAAU,QAAQ,MAAM,OAAO,YAAY,GACjD,SACA,IACD;AAAA,EACD,MAAM,WAAW;AAAA,EACjB,OAAO;AAAA;AASR,SAAS,IAAI,CACZ,OACA,MACA,OACA,cACO;AAAA,EACP,MAAM,YAAa,MAAM,cACvB,eAAe,uBAAuB;AAAA,EACxC,IAAI,WAAW;AAAA,IACd,IAAI,CAAC,UAAU;AAAA,MAAU,MAAM,YAAY,SAAS,MAAM,OAAO,SAAS;AAAA,IAC1E;AAAA,EACD;AAAA,EACA,MAAM,UAAW,MAAM,WAAW;AAAA,EAClC,IAAI,CAAC,QAAQ;AAAA,IAAU,MAAM,UAAU,SAAS,MAAM,OAAO,OAAO;AAAA;AA0B9D,SAAS,cAAuC,CAAC,QAAc;AAAA,EACrE,MAAM,YAAY,kBAAiB,QAAQ,CAAC,MAAM,OAAO,SAAS;AAAA,IACjE,MAAM,eAAe,SAAS,OAAO,oBAAoB;AAAA,IACzD,KAAK,OAAO,MAAM,GAAG,KAAK,QAAQ,MAAM,QAAQ,YAAY;AAAA,GAC5D;AAAA,EACD,IAAI,CAAC,WAAW;AAAA,IACf,MAAM,IAAI,MACT,gHACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA;",
10
- "debugId": "EAADEEA4CBC8F7A764756E2164756E21",
9
+ "mappings": "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",
10
+ "debugId": "B588BD3EF63841CA64756E2164756E21",
11
11
  "names": []
12
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/rules/format.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,UAAU,qDASrB,CAAC"}
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/rules/format.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,qDAmBrB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import type { z } from 'zod';
2
2
  import * as all from './all.js';
3
+ import { type RuleContext } from './rule.js';
3
4
  /**
4
5
  * One of the rules, as its own type: a builder narrows it on `target`, so a
5
6
  * number schema never reaches a string rule.
@@ -20,9 +21,13 @@ export declare const rules: { readonly [A in ConstraintArgument]: Extract<Regist
20
21
  * target: the one place the rule's typed `toZod` meets a schema built from
21
22
  * GraphQL types, which the compiler cannot relate.
22
23
  */
23
- export declare function applyRule(rule: RegisteredRule, schema: z.ZodType, value: string | number): z.ZodType;
24
+ export declare function applyRule(rule: RegisteredRule, schema: z.ZodType, value: string | number, context: RuleContext): z.ZodType;
24
25
  /** `applyRule` for source: the rule's typed `toCode` on a schema's source. */
25
- export declare function applyRuleCode(rule: RegisteredRule, source: string, value: string | number): string;
26
- /** The `@constraint` argument whose rule refused with this issue, if any. */
26
+ export declare function applyRuleCode(rule: RegisteredRule, source: string, value: string | number, context: RuleContext): string;
27
+ /**
28
+ * The `@constraint` argument whose rule refused with this issue, if any: the
29
+ * one the issue is marked with (an application format's), else the one rule
30
+ * that owns its shape.
31
+ */
27
32
  export declare function constraintOf(issue: z.core.$ZodIssue): ConstraintArgument | undefined;
28
33
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAG7B;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC;AAE5D,uCAAuC;AACvC,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAEb,EACJ,QAAQ,EAAE,CAAC,IAAI,kBAAkB,GAAG,OAAO,CAAC,cAAc,EAAE;IAAE,QAAQ,EAAE,CAAC,CAAA;CAAE,CAAC,GAC5E,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,SAAS,CACxB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,CAAC,CAAC,OAAO,EACjB,KAAK,EAAE,MAAM,GAAG,MAAM,GACpB,CAAC,CAAC,OAAO,CAMX;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAC5B,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,MAAM,GACpB,MAAM,CAMR;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAC3B,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,GACrB,kBAAkB,GAAG,SAAS,CAGhC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AAExE;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC;AAE5D,uCAAuC;AACvC,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAEb,EACJ,QAAQ,EAAE,CAAC,IAAI,kBAAkB,GAAG,OAAO,CAAC,cAAc,EAAE;IAAE,QAAQ,EAAE,CAAC,CAAA;CAAE,CAAC,GAC5E,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,SAAS,CACxB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,CAAC,CAAC,OAAO,EACjB,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,OAAO,EAAE,WAAW,GAClB,CAAC,CAAC,OAAO,CAOX;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAC5B,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,OAAO,EAAE,WAAW,GAClB,MAAM,CAOR;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,GACrB,kBAAkB,GAAG,SAAS,CAMhC"}
@@ -1,5 +1,6 @@
1
1
  import type { z } from 'zod';
2
2
  import type { StringSchema } from '../formats/format.js';
3
+ import type { FormatRegistry } from '../formats/registry.js';
3
4
  /** The schema each kind of rule narrows. */
4
5
  export interface Targets {
5
6
  string: StringSchema;
@@ -7,6 +8,15 @@ export interface Targets {
7
8
  list: z.ZodArray<z.ZodType>;
8
9
  }
9
10
  export type Target = keyof Targets;
11
+ /**
12
+ * What a rule may read besides its value: the formats `format` resolves
13
+ * against, the built-in ones and the application's own, and, when writing
14
+ * source, how to write one of the application's (`InputCodeOptions.format`).
15
+ */
16
+ export interface RuleContext {
17
+ readonly formats: FormatRegistry;
18
+ readonly formatCode?: ((name: string) => string) | undefined;
19
+ }
10
20
  /** What a `@constraint` argument's value can be. */
11
21
  interface ValueOf {
12
22
  Int: number;
@@ -27,12 +37,12 @@ export interface Rule<A extends string = string, T extends keyof ValueOf = keyof
27
37
  readonly type: T;
28
38
  readonly target: K;
29
39
  readonly base?: true;
30
- readonly toZod: (schema: Targets[K], value: ValueOf[T]) => Targets[K];
40
+ readonly toZod: (schema: Targets[K], value: ValueOf[T], context: RuleContext) => Targets[K];
31
41
  /**
32
42
  * `schema` is the source of the schema this rule narrows. The source names
33
43
  * zod as a free `z`: the generated file imports it.
34
44
  */
35
- readonly toCode: (schema: string, value: ValueOf[T]) => string;
45
+ readonly toCode: (schema: string, value: ValueOf[T], context: RuleContext) => string;
36
46
  /**
37
47
  * Whether a Zod issue is this rule's refusal, so the error a client gets
38
48
  * names the rule (`constraint: 'minLength'`), which does not change with
@@ -46,7 +56,14 @@ export interface IssueFields {
46
56
  readonly origin?: string;
47
57
  readonly format?: string;
48
58
  readonly inclusive?: boolean;
59
+ readonly params?: Readonly<Record<string, unknown>>;
49
60
  }
61
+ /**
62
+ * The `params` key that marks an issue as one argument's whatever its code:
63
+ * an application format's issues carry `format` there, so a `.regex()` or a
64
+ * `.refine()` inside one is not read as `pattern` or `notContains`.
65
+ */
66
+ export declare const RULE_PARAM = "nxgtConstraint";
50
67
  /** Declares a rule, with its argument name kept literal. */
51
68
  export declare function defineRule<const A extends string, T extends keyof ValueOf, K extends Target>(rule: Rule<A, T, K>): Rule<A, T, K>;
52
69
  /** A string literal as source: quotes and escapes included. */
@@ -1 +1 @@
1
- {"version":3,"file":"rule.d.ts","sourceRoot":"","sources":["../../src/rules/rule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,4CAA4C;AAC5C,MAAM,WAAW,OAAO;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;CAC5B;AAED,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;AAEnC,oDAAoD;AACpD,UAAU,OAAO;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,IAAI,CACpB,CAAC,SAAS,MAAM,GAAG,MAAM,EACzB,CAAC,SAAS,MAAM,OAAO,GAAG,MAAM,OAAO,EACvC,CAAC,SAAS,MAAM,GAAG,MAAM;IAEzB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACtE;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC;CAC/C;AAED,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,4DAA4D;AAC5D,wBAAgB,UAAU,CACzB,KAAK,CAAC,CAAC,SAAS,MAAM,EACtB,CAAC,SAAS,MAAM,OAAO,EACvB,CAAC,SAAS,MAAM,EACf,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAEpC;AAED,+DAA+D;AAC/D,eAAO,MAAM,OAAO,GAAI,OAAO,MAAM,KAAG,MAA+B,CAAC"}
1
+ {"version":3,"file":"rule.d.ts","sourceRoot":"","sources":["../../src/rules/rule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,4CAA4C;AAC5C,MAAM,WAAW,OAAO;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;CAC5B;AAED,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;AAEnC;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,CAAC;CAC7D;AAED,oDAAoD;AACpD,UAAU,OAAO;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,IAAI,CACpB,CAAC,SAAS,MAAM,GAAG,MAAM,EACzB,CAAC,SAAS,MAAM,OAAO,GAAG,MAAM,OAAO,EACvC,CAAC,SAAS,MAAM,GAAG,MAAM;IAEzB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,CACf,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAClB,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EACjB,OAAO,EAAE,WAAW,KAChB,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,CAChB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EACjB,OAAO,EAAE,WAAW,KAChB,MAAM,CAAC;IACZ;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC;CAC/C;AAED,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACpD;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,mBAAmB,CAAC;AAE3C,4DAA4D;AAC5D,wBAAgB,UAAU,CACzB,KAAK,CAAC,CAAC,SAAS,MAAM,EACtB,CAAC,SAAS,MAAM,OAAO,EACvB,CAAC,SAAS,MAAM,EACf,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAEpC;AAED,+DAA+D;AAC/D,eAAO,MAAM,OAAO,GAAI,OAAO,MAAM,KAAG,MAA+B,CAAC"}
@@ -1,4 +1,15 @@
1
1
  import { type GraphQLSchema } from 'graphql';
2
+ import type { FormatSchemas, OwnFormats } from './formats/registry.js';
3
+ /** What `withValidation` takes besides the schema. */
4
+ export interface WithValidationOptions<F extends FormatSchemas = FormatSchemas> {
5
+ /**
6
+ * The application's own formats, which `@constraint(format: "...")` may
7
+ * name beside the built-in ones: one Zod string schema each, keyed by its
8
+ * name (lowercase letters, digits and hyphens). A built-in name is a type
9
+ * error, and refused at startup.
10
+ */
11
+ readonly formats?: OwnFormats<F>;
12
+ }
2
13
  /**
3
14
  * Checks every `@constraint` of `schema` before the resolvers run. Each
4
15
  * field with a constrained argument — directly, or anywhere inside an input
@@ -21,6 +32,11 @@ import { type GraphQLSchema } from 'graphql';
21
32
  * The directives are read from the SDL (`astNode`), so a schema built from
22
33
  * type definitions — `buildSchema`, `makeExecutableSchema`, Yoga, Apollo —
23
34
  * is checked; a code-first schema with no SDL has no `@constraint` to read.
35
+ *
36
+ * `options.formats` adds the application's own formats; wrapping a schema
37
+ * again throws unless the record has the same names, each the very same
38
+ * schema (no formats after some, or some after none, throws too), since its
39
+ * fields are already checked against the first ones.
24
40
  */
25
- export declare function withValidation<S extends GraphQLSchema>(schema: S): S;
41
+ export declare function withValidation<S extends GraphQLSchema, const F extends FormatSchemas = {}>(schema: S, options?: WithValidationOptions<F>): S;
26
42
  //# sourceMappingURL=with-validation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"with-validation.d.ts","sourceRoot":"","sources":["../src/with-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,KAAK,aAAa,EAClB,MAAM,SAAS,CAAC;AAiDjB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,aAAa,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAWpE"}
1
+ {"version":3,"file":"with-validation.d.ts","sourceRoot":"","sources":["../src/with-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,KAAK,aAAa,EAClB,MAAM,SAAS,CAAC;AAIjB,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQpE,sDAAsD;AACtD,MAAM,WAAW,qBAAqB,CACrC,CAAC,SAAS,aAAa,GAAG,aAAa;IAEvC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CACjC;AA0CD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,cAAc,CAC7B,CAAC,SAAS,aAAa,EAEvB,KAAK,CAAC,CAAC,SAAS,aAAa,GAAG,EAAE,EACjC,MAAM,EAAE,CAAC,EAAE,OAAO,GAAE,qBAAqB,CAAC,CAAC,CAAM,GAAG,CAAC,CAwBtD"}
package/docs/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  | Page | Read it when |
6
6
  | --- | --- |
7
- | [Constraints](guide/constraints.md) | you write `@constraint` in your schema: setup with `withValidation`, every argument and its Zod equivalent, the formats, lists and nested inputs |
7
+ | [Constraints](guide/constraints.md) | you write `@constraint` in your schema: setup with `withValidation`, every argument and its Zod equivalent, the formats and your own, lists and nested inputs |
8
8
  | [Errors](guide/errors.md) | you handle the `BAD_USER_INPUT` error in a client, build it yourself with `badUserInput`, or add rules a directive cannot say with `validated` |
9
9
 
10
10
  ## Reference
@@ -76,8 +76,12 @@ What `withValidation` does:
76
76
 
77
77
  ```ts
78
78
  import type { GraphQLSchema } from 'graphql';
79
+ import type { FormatSchemas } from '@nxgt/graphql-validation';
79
80
 
80
- declare function withValidation<S extends GraphQLSchema>(schema: S): S;
81
+ declare function withValidation<S extends GraphQLSchema>(
82
+ schema: S,
83
+ options?: { formats?: FormatSchemas }, // your own formats, below
84
+ ): S;
81
85
  declare const constraintTypeDefs: string;
82
86
  ```
83
87
 
@@ -188,9 +192,10 @@ email: String! @constraint(maxLength: 12, format: "email")
188
192
 
189
193
  ## Formats
190
194
 
191
- `@constraint(format: "...")` takes one of these names. Anything else fails at
192
- startup with `Unknown @constraint format "siret". Known formats: byte, date,
193
- date-time, email, ipv4, ipv6, uri, uuid.`
195
+ `@constraint(format: "...")` takes one of these names, or one of
196
+ [your own formats](#your-own-formats). Anything else fails at startup with
197
+ `Unknown @constraint format "siret". Known formats: byte, date, date-time,
198
+ email, ipv4, ipv6, uri, uuid.`, your own formats listed after these.
194
199
 
195
200
  | Format | Zod schema | Accepts |
196
201
  | --- | --- | --- |
@@ -209,6 +214,130 @@ Where they differ from graphql-constraint-directive's (validator.js):
209
214
  present. A lowercase `t` or `z`, or a space for `T`, is refused.
210
215
  - `uri` requires the scheme (`example.com` is refused) and nothing but `http`,
211
216
  `https` and `ftp` gets in: no `javascript:`, no `mailto:`.
217
+ - `uri` trims surrounding white space, as `z.url()` does: for
218
+ `" https://a.com "` the resolver receives `"https://a.com"`, and a
219
+ `maxLength` written after it counts the trimmed value. It is the one
220
+ format whose value reaches the resolver changed; a client generated by
221
+ `@nxgt/graphql-codegen-zod` checks the same trimmed value, so both agree.
222
+ Your own formats cannot do this: a format that rewrites is refused.
223
+
224
+ ## Your own formats
225
+
226
+ When no built-in format says it, write the format as a Zod string schema and
227
+ name it. Keep every format in one module, exported as one record:
228
+
229
+ ```ts
230
+ // formats.ts
231
+ import { z } from 'zod';
232
+
233
+ export const formatSchemas = {
234
+ // A French company number: 14 digits.
235
+ siret: z.string().regex(/^\d{14}$/, 'Invalid SIRET: write its 14 digits'),
236
+ // An address at your company only.
237
+ 'work-email': z.email().endsWith('@example.com', 'Use your work address'),
238
+ // Any check Zod can write, as long as it changes nothing.
239
+ 'even-code': z
240
+ .string()
241
+ .refine((value) => value.length % 2 === 0, 'Invalid code: an even length'),
242
+ };
243
+ ```
244
+
245
+ Hand the record to `withValidation`:
246
+
247
+ ```ts
248
+ import { constraintTypeDefs, withValidation } from '@nxgt/graphql-validation';
249
+ import { buildSchema } from 'graphql';
250
+ import { formatSchemas } from './formats';
251
+
252
+ const schema = withValidation(
253
+ buildSchema(/* GraphQL */ `
254
+ ${constraintTypeDefs}
255
+ input Company {
256
+ siret: String! @constraint(format: "siret")
257
+ contact: String @constraint(format: "work-email", maxLength: 64)
258
+ }
259
+ type Query { company(input: Company!): String }
260
+ `),
261
+ { formats: formatSchemas },
262
+ );
263
+ ```
264
+
265
+ What a format of your own is:
266
+
267
+ - **A Zod string schema**: `z.string()` or a string format (`z.email()`,
268
+ `z.uuid()`, …), narrowed with anything that keeps it a string schema
269
+ (`.regex()`, `.min()`, `.endsWith()`, `.refine()`). Never a `.transform()`,
270
+ a codec or `.optional()`: the resolver receives what the client sent, and a
271
+ null or absent value is the field's type's business (`String` or `String!`),
272
+ not the format's. A non-string schema is a type error, and refused at startup.
273
+ - **It never rewrites the value.** These keep a string schema but change
274
+ the value it passes on, and are refused at startup:
275
+ - `.trim()`, `.toLowerCase()`, `.toUpperCase()`, `.normalize()`,
276
+ `.slugify()` and `.overwrite()`;
277
+ - `z.url()`, `z.httpUrl()` and `.url()`, which trim the value and drop its
278
+ tabs and newlines (`z.url({ normalize: true })` rewrites it whole);
279
+ - `z.coerce.string()`, which turns `12345` into `"12345"` (a type error
280
+ too: it takes `unknown`).
281
+
282
+ The server checks the value as sent, while a client chaining `maxLength` on
283
+ your schema would check the rewritten one, so the two would disagree on
284
+ `" AB "` or `" https://a.com "`. Every other string format (`z.email()`,
285
+ `z.uuid()`, `z.iso.datetime()`, …) only checks. Refuse what is not
286
+ canonical instead:
287
+
288
+ ```ts
289
+ // not z.string().trim().toLowerCase()
290
+ const slug = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Invalid slug: lowercase words joined by hyphens');
291
+ // not z.url()
292
+ const link = z.string().refine((value) => value.trim() === value && URL.canParse(value), 'Invalid URL');
293
+ ```
294
+
295
+ A custom `.check()` that sets `ctx.value` shows nothing in its definition,
296
+ so startup cannot see it: the request it rewrites fails, naming the format
297
+ (`The format "slug" rewrote the value it checked…`), as a bug in a resolver
298
+ would, and the resolver is not called.
299
+
300
+ - **Narrowed like a built-in one.** `format` comes first and the other rules
301
+ narrow it: `@constraint(format: "work-email", maxLength: 64)` is your schema,
302
+ then at most 64 characters. A check of yours with `abort: true`
303
+ (`.refine(…, { message, abort: true })`) stops the rules after it, on the
304
+ server as in a generated client: only its issue is returned. Once an async
305
+ check of your format is pending, the rules have already run on the server:
306
+ their issues come first (the error's message is then a rule's, not your
307
+ format's) and the abort stops nothing. Put an aborting check before any
308
+ async one.
309
+ - **Its messages are yours.** A refusal reads as your schema writes it (the
310
+ package's own messages never name the value; yours may, so keep the input
311
+ out of them if your logs or clients should not see it). Every issue your
312
+ schema raises carries `constraint: "format"`, whatever its Zod `code` — a
313
+ `.regex()` inside your format is the format's refusal, not `pattern`'s:
314
+
315
+ ```json
316
+ { "path": ["input", "siret"], "message": "Invalid SIRET: write its 14 digits", "code": "invalid_format", "constraint": "format" }
317
+ ```
318
+
319
+ - **Named in lowercase letters, digits and hyphens**, starting with a letter
320
+ (`siret`, `work-email`, `iso-6346`). A built-in name (`email`, `uuid`, …)
321
+ cannot be replaced: it is a type error, and refused at startup.
322
+ - **An async check stays async.** `withValidation` parses asynchronously, so a
323
+ `.refine(async …)` works in a request, from this package's zod or another
324
+ copy. It runs once per value, and when it rejects (`db down`) the operation
325
+ fails with that error, as a resolver's would. A default value is checked
326
+ at startup synchronously, so a field whose format is async cannot have a
327
+ default: startup throws `The default value of Query.a(s:) cannot be checked
328
+ at startup: the format "free" checks asynchronously, …`.
329
+
330
+ Call `withValidation` once, with every format. A second call on the same
331
+ schema wraps nothing when its record has the same names, each the very same
332
+ schema object (the same record, or a copy of it); otherwise it throws — other
333
+ names, another schema object for a name (even an identical one), or no
334
+ `formats` after a call with some, or the reverse — since the fields are already
335
+ checked against the first formats. Without `formats`, nothing changes.
336
+
337
+ [`@nxgt/graphql-codegen-zod`](https://www.npmjs.com/package/@nxgt/graphql-codegen-zod)
338
+ 0.3 reads the same record (`config: { formatSchemas: './formats' }`): the
339
+ generated schemas import your formats and chain the other rules on them, so
340
+ the client refuses what the server refuses, with your messages.
212
341
 
213
342
  ## In your IDE
214
343
 
@@ -38,7 +38,10 @@ arguments**: the argument name, then input fields, with a number for a list
38
38
  index. `code` is Zod's issue code (`too_small`, `too_big`, `invalid_format`,
39
39
  `custom`, ...). `constraint` is the `@constraint` argument that refused
40
40
  (`minLength`, `format`, `maxItems`, ...); it is absent for a refusal of a
41
- [`validated`](#validated) schema. `message` is Zod's, in English, and is not
41
+ [`validated`](#validated) schema. A refusal of one of
42
+ [your own formats](constraints.md#your-own-formats) is `format` whatever its
43
+ `code` (a `.regex()` or `.refine()` inside it included), and its `message`
44
+ is your schema's. Otherwise `message` is Zod's, in English, and is not
42
45
  meant for display in every language: map on `constraint` and `path` if you
43
46
  translate. Prefer `constraint` to `code`: it is the name in your schema, while
44
47
  `code` is Zod's and can change when Zod does across a major version.
package/docs/roadmap.md CHANGED
@@ -4,22 +4,14 @@ What `@nxgt/graphql-validation` is heading for, phrased as what you get.
4
4
 
5
5
  ## Now
6
6
 
7
- - **`@constraint` on arguments and input fields.** `constraintTypeDefs` for
8
- your SDL, then `withValidation(schema)`: every constrained argument is
9
- checked by a Zod schema built from its directives, through nested input
10
- types and lists, before your resolver runs. The resolver receives the
11
- parsed value.
12
- - **One error a form can use.** An invalid input is one `GraphQLError` with
13
- `extensions.code` `BAD_USER_INPUT` and an `issues` list, each with the
14
- path of the offending field inside the arguments.
15
- - **`validated(schema, resolver)`** for what a directive cannot say: rules
16
- across fields, business refinements, with the same error.
17
- - **Formats** `byte`, `date-time`, `date`, `email`, `ipv4`, `ipv6`, `uri`,
18
- `uuid`.
7
+ - **Your own formats** (in progress, not released yet). Name a Zod string schema in
8
+ `@constraint(format: "...")`: `withValidation(schema, { formats })`, one
9
+ record your module exports, checked at startup, its refusals named
10
+ `format`. `@nxgt/graphql-codegen-zod` 0.3 reads the same record, so the
11
+ client refuses what the server refuses, with the same messages.
19
12
 
20
13
  ## Next
21
14
 
22
- - **Your own formats**, named in `@constraint(format: "...")`.
23
15
  - **Schemas that read the context**, `(args, context) => schema`, for checks
24
16
  that need the request (uniqueness in a database, permissions).
25
17
 
@@ -27,7 +19,19 @@ What `@nxgt/graphql-validation` is heading for, phrased as what you get.
27
19
 
28
20
  - **Declarations resolve under `nodenext`** — 0.2.1. `moduleResolution`
29
21
  `nodenext` and `node16` find every export, as `bundler` does.
30
- - **A Zod codegen plugin**, [`@nxgt/graphql-codegen-zod`](https://www.npmjs.com/package/@nxgt/graphql-codegen-zod):
31
- it writes the same schemas as source, from the same rules, so the types your
22
+ - **A Zod codegen plugin** — 0.2.0. [`@nxgt/graphql-codegen-zod`](https://www.npmjs.com/package/@nxgt/graphql-codegen-zod)
23
+ writes the same schemas as source, from the same rules, so the types your
32
24
  resolvers receive and the variables a client sends come from your SDL. This
33
25
  package gains `@nxgt/graphql-validation/codegen` for it.
26
+ - **`@constraint` on arguments and input fields** — 0.1.0.
27
+ `constraintTypeDefs` for your SDL, then `withValidation(schema)`: every
28
+ constrained argument is checked by a Zod schema built from its directives,
29
+ through nested input types and lists, before your resolver runs. The
30
+ resolver receives the parsed value.
31
+ - **One error a form can use** — 0.1.0. An invalid input is one
32
+ `GraphQLError` with `extensions.code` `BAD_USER_INPUT` and an `issues`
33
+ list, each with the path of the offending field inside the arguments.
34
+ - **`validated(schema, resolver)`** — 0.1.0. For what a directive cannot
35
+ say: rules across fields, business refinements, with the same error.
36
+ - **Formats** — 0.1.0. `byte`, `date-time`, `date`, `email`, `ipv4`, `ipv6`,
37
+ `uri`, `uuid`.