@nxgt/graphql-validation 0.2.1 → 0.3.1
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 +64 -5
- package/dist/builder/check-constraints.d.ts +6 -1
- package/dist/builder/check-constraints.d.ts.map +1 -1
- package/dist/builder/input-code.d.ts +14 -1
- package/dist/builder/input-code.d.ts.map +1 -1
- package/dist/builder/input-schema.d.ts +3 -1
- package/dist/builder/input-schema.d.ts.map +1 -1
- package/dist/builder/leaf.d.ts +2 -2
- package/dist/builder/leaf.d.ts.map +1 -1
- package/dist/chunks/index-twevz7yc.js +527 -0
- package/dist/chunks/index-twevz7yc.js.map +26 -0
- package/dist/chunks/{index-rbxe5m3y.js → index-xcaf9kes.js} +24 -97
- package/dist/chunks/index-xcaf9kes.js.map +27 -0
- package/dist/cli.js +1 -1
- package/dist/codegen.d.ts +1 -0
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +19 -9
- package/dist/codegen.js.map +3 -3
- package/dist/formats/byte.d.ts +1 -1
- package/dist/formats/byte.d.ts.map +1 -1
- package/dist/formats/date-time.d.ts +1 -1
- package/dist/formats/date-time.d.ts.map +1 -1
- package/dist/formats/date.d.ts +1 -1
- package/dist/formats/date.d.ts.map +1 -1
- package/dist/formats/email.d.ts +1 -1
- package/dist/formats/email.d.ts.map +1 -1
- package/dist/formats/format.d.ts +17 -3
- package/dist/formats/format.d.ts.map +1 -1
- package/dist/formats/index.d.ts +6 -8
- package/dist/formats/index.d.ts.map +1 -1
- package/dist/formats/ipv4.d.ts +1 -1
- package/dist/formats/ipv4.d.ts.map +1 -1
- package/dist/formats/ipv6.d.ts +1 -1
- package/dist/formats/ipv6.d.ts.map +1 -1
- package/dist/formats/marked.d.ts +42 -0
- package/dist/formats/marked.d.ts.map +1 -0
- package/dist/formats/registry.d.ts +50 -0
- package/dist/formats/registry.d.ts.map +1 -0
- package/dist/formats/uri.d.ts +1 -1
- package/dist/formats/uri.d.ts.map +1 -1
- package/dist/formats/uuid.d.ts +1 -1
- package/dist/formats/uuid.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -5
- package/dist/index.js.map +3 -3
- package/dist/rules/format.d.ts.map +1 -1
- package/dist/rules/index.d.ts +8 -3
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/rule.d.ts +19 -2
- package/dist/rules/rule.d.ts.map +1 -1
- package/dist/with-validation.d.ts +17 -1
- package/dist/with-validation.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/guide/constraints.md +149 -4
- package/docs/guide/errors.md +4 -1
- package/docs/roadmap.md +19 -15
- package/docs/troubleshooting.md +157 -4
- package/package.json +2 -2
- package/dist/chunks/index-rbxe5m3y.js.map +0 -37
- package/dist/chunks/index-s1kkmg35.js +0 -266
- package/dist/chunks/index-s1kkmg35.js.map +0 -14
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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-
|
|
4
|
+
} from "./chunks/index-xcaf9kes.js";
|
|
5
5
|
import {
|
|
6
6
|
checkConstraints2
|
|
7
|
-
} from "./chunks/index-
|
|
7
|
+
} from "./chunks/index-twevz7yc.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=
|
|
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
|
|
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;
|
|
10
|
-
"debugId": "
|
|
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":"
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/rules/format.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,qDAmBrB,CAAC"}
|
package/dist/rules/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
|
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"}
|
package/dist/rules/rule.d.ts
CHANGED
|
@@ -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. */
|
package/dist/rules/rule.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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>(
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
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,146 @@ 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, a client has already run the rules, so
|
|
306
|
+
the abort stops nothing on either side; the server lists your format's
|
|
307
|
+
issues first. Put an aborting check before any async one.
|
|
308
|
+
- **Its messages are yours.** A refusal reads as your schema writes it (the
|
|
309
|
+
package's own messages never name the value; yours may, so keep the input
|
|
310
|
+
out of them if your logs or clients should not see it). Every issue your
|
|
311
|
+
schema raises carries `constraint: "format"`, whatever its Zod `code` — a
|
|
312
|
+
`.regex()` inside your format is the format's refusal, not `pattern`'s:
|
|
313
|
+
|
|
314
|
+
```json
|
|
315
|
+
{ "path": ["input", "siret"], "message": "Invalid SIRET: write its 14 digits", "code": "invalid_format", "constraint": "format" }
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- **Named in lowercase letters, digits and hyphens**, starting with a letter
|
|
319
|
+
(`siret`, `work-email`, `iso-6346`). A built-in name (`email`, `uuid`, …)
|
|
320
|
+
cannot be replaced: it is a type error, and refused at startup.
|
|
321
|
+
- **An async check stays async, and runs once.** `withValidation` parses
|
|
322
|
+
asynchronously, so a `.refine(async …)` (or an async `.superRefine()`, or
|
|
323
|
+
any check returning a promise) works in a request, from this package's zod
|
|
324
|
+
or another copy. It runs once per value, from the first, and when it
|
|
325
|
+
rejects (`db down`) the operation fails with that error, as a resolver's
|
|
326
|
+
would: no rejection is left unhandled. A default value is checked at
|
|
327
|
+
startup synchronously, so a field whose format is async cannot have a
|
|
328
|
+
default: startup throws `The default value of Query.a(s:) cannot be checked
|
|
329
|
+
at startup: the format "free" checks asynchronously, …`. A check that
|
|
330
|
+
throws on the default value reads the same, since its error ends a run
|
|
331
|
+
startup cannot wait for.
|
|
332
|
+
|
|
333
|
+
- **Only Zod's public API runs it.** Your format is parsed with
|
|
334
|
+
`safeParseAsync` only, never a synchronous `safeParse` that would start an
|
|
335
|
+
async check and drop it; a `.catch()` and `.refine()`s chained after your
|
|
336
|
+
checks tell whether the run finished at once and which abort stopped it,
|
|
337
|
+
and the rules after it run inside it, replayed on its issues as Zod runs
|
|
338
|
+
them when chained on your schema. No Zod internal is called, so a Zod 4
|
|
339
|
+
release that changes its internals changes nothing here, and a format
|
|
340
|
+
from another copy of Zod (another version included) gets its messages
|
|
341
|
+
from that copy's configuration (`z.config()`). Startup still reads the
|
|
342
|
+
schema's definition (`_zod.def`, Zod's introspection) to refuse what
|
|
343
|
+
rewrites the value. The cost, measured on Bun 1.4.2 against 0.3.0: about
|
|
344
|
+
0.1 µs more per accepted value and 1.5 µs more per refused one.
|
|
345
|
+
|
|
346
|
+
Call `withValidation` once, with every format. A second call on the same
|
|
347
|
+
schema wraps nothing when its record has the same names, each the very same
|
|
348
|
+
schema object (the same record, or a copy of it); otherwise it throws — other
|
|
349
|
+
names, another schema object for a name (even an identical one), or no
|
|
350
|
+
`formats` after a call with some, or the reverse — since the fields are already
|
|
351
|
+
checked against the first formats. Without `formats`, nothing changes.
|
|
352
|
+
|
|
353
|
+
[`@nxgt/graphql-codegen-zod`](https://www.npmjs.com/package/@nxgt/graphql-codegen-zod)
|
|
354
|
+
0.3 reads the same record (`config: { formatSchemas: './formats' }`): the
|
|
355
|
+
generated schemas import your formats and chain the other rules on them, so
|
|
356
|
+
the client refuses what the server refuses, with your messages.
|
|
212
357
|
|
|
213
358
|
## In your IDE
|
|
214
359
|
|
package/docs/guide/errors.md
CHANGED
|
@@ -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.
|
|
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
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
31
|
-
|
|
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`.
|