@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.
- package/README.md +62 -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-s1kkmg35.js → index-1mzzydy3.js} +219 -23
- package/dist/chunks/index-1mzzydy3.js.map +25 -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 +9 -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/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 +133 -4
- package/docs/guide/errors.md +4 -1
- package/docs/roadmap.md +19 -15
- package/docs/troubleshooting.md +151 -4
- package/package.json +2 -2
- package/dist/chunks/index-rbxe5m3y.js.map +0 -37
- package/dist/chunks/index-s1kkmg35.js.map +0 -14
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Peers, all **required**:
|
|
|
19
19
|
| --- | --- |
|
|
20
20
|
| `graphql` | `^16.11.0 \|\| ^17.0.0` |
|
|
21
21
|
| `zod` | `>=4.6.5 <5` |
|
|
22
|
-
| `typescript` | `^6.0.3` |
|
|
22
|
+
| `typescript` | `^6.0.3 \|\| ^7.0.0` |
|
|
23
23
|
|
|
24
24
|
Your `tsconfig.json` needs:
|
|
25
25
|
|
|
@@ -83,6 +83,61 @@ request. The resolver receives the parsed arguments.
|
|
|
83
83
|
Every argument, its rule and its Zod equivalent, and the formats, are in
|
|
84
84
|
[Constraints](docs/guide/constraints.md).
|
|
85
85
|
|
|
86
|
+
### Your own formats
|
|
87
|
+
|
|
88
|
+
Name a format of your own in `@constraint(format: "...")`: write each as a Zod
|
|
89
|
+
string schema in one module, and hand the record to `withValidation`.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// formats.ts
|
|
93
|
+
import { z } from 'zod';
|
|
94
|
+
|
|
95
|
+
export const formatSchemas = {
|
|
96
|
+
siret: z.string().regex(/^\d{14}$/, 'Invalid SIRET: write its 14 digits'),
|
|
97
|
+
'work-email': z.email().endsWith('@example.com', 'Use your work address'),
|
|
98
|
+
};
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { makeExecutableSchema } from '@graphql-tools/schema';
|
|
103
|
+
import { constraintTypeDefs, withValidation } from '@nxgt/graphql-validation';
|
|
104
|
+
import { formatSchemas } from './formats';
|
|
105
|
+
|
|
106
|
+
const typeDefs = [
|
|
107
|
+
constraintTypeDefs,
|
|
108
|
+
/* GraphQL */ `
|
|
109
|
+
input Company {
|
|
110
|
+
siret: String! @constraint(format: "siret")
|
|
111
|
+
contact: String @constraint(format: "work-email", maxLength: 64)
|
|
112
|
+
}
|
|
113
|
+
type Mutation { register(input: Company!): Boolean }
|
|
114
|
+
`,
|
|
115
|
+
];
|
|
116
|
+
const resolvers = { Mutation: { register: () => true } };
|
|
117
|
+
|
|
118
|
+
export const schema = withValidation(
|
|
119
|
+
makeExecutableSchema({ typeDefs, resolvers }),
|
|
120
|
+
{ formats: formatSchemas },
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A format is `z.string()` or a string format (`z.email()`, …), narrowed as you
|
|
125
|
+
like (`.regex()`, `.refine()`) but never transformed nor rewritten (no
|
|
126
|
+
`.transform()`, `.trim()`, `.toLowerCase()`, `z.url()`, which trims,
|
|
127
|
+
`z.coerce.string()`, …): the resolver receives what the client sent. The one
|
|
128
|
+
exception is the built-in `uri`, `z.url()`, which trims surrounding white
|
|
129
|
+
space (see [Formats](docs/guide/constraints.md#formats)). The other rules
|
|
130
|
+
narrow your format as they narrow a built-in one. Its messages are yours: a
|
|
131
|
+
refusal reads as your schema writes it, and its issue carries
|
|
132
|
+
`constraint: "format"`. A name is lowercase letters, digits and hyphens; a
|
|
133
|
+
built-in name (`email`, …) is a type error and refused at startup, and so is
|
|
134
|
+
a schema that rewrites the value (a custom `.check()` that does fails the
|
|
135
|
+
request, naming the format). An async check (`.refine(async …)`) works in a
|
|
136
|
+
request, and runs once per value. Call `withValidation` once, with every
|
|
137
|
+
format: a second call with the same record wraps nothing; with other
|
|
138
|
+
formats, or none, it throws. Without `formats`, nothing changes. Details in
|
|
139
|
+
[Constraints](docs/guide/constraints.md#your-own-formats).
|
|
140
|
+
|
|
86
141
|
### Read the error
|
|
87
142
|
|
|
88
143
|
```json
|
|
@@ -171,12 +226,13 @@ the directive declared is this package's. Both at once throw
|
|
|
171
226
|
| Export | Is |
|
|
172
227
|
| --- | --- |
|
|
173
228
|
| `constraintTypeDefs` | the SDL of `@constraint`, to add to `typeDefs` |
|
|
174
|
-
| `withValidation(schema)` | checks every `@constraint` of a schema before its resolvers run |
|
|
229
|
+
| `withValidation(schema, options?)` | checks every `@constraint` of a schema before its resolvers run; `options.formats` adds your own formats |
|
|
175
230
|
| `validated(schema \| shape, resolver)` | a resolver whose arguments a Zod schema checks |
|
|
176
231
|
| `badUserInput(where, zodError)` | builds the error above, for code that validates by hand |
|
|
177
232
|
| `ValidationIssue`, `BadUserInputExtensions` | the types of `extensions.issues` and `extensions` |
|
|
178
233
|
| `ConstraintArgument` | the type of `issues[].constraint`: `'minLength' \| 'format' \| ...` |
|
|
179
234
|
| `ArgsSchema`, `SchemaOf` | the types `validated` accepts |
|
|
235
|
+
| `WithValidationOptions`, `FormatSchemas`, `OwnFormats`, `StringSchema`, `FormatName` | the types of `withValidation`'s options: a record of string schemas, with no built-in format name (`FormatName`) |
|
|
180
236
|
|
|
181
237
|
`@nxgt/graphql-validation/codegen` is for a code generator, such as
|
|
182
238
|
[`@nxgt/graphql-codegen-zod`](https://www.npmjs.com/package/@nxgt/graphql-codegen-zod): it writes the schemas `withValidation` builds as source, and
|
|
@@ -184,9 +240,10 @@ refuses the schemas `withValidation` refuses. A server does not need it.
|
|
|
184
240
|
|
|
185
241
|
| Export | Is |
|
|
186
242
|
| --- | --- |
|
|
187
|
-
| `checkConstraints(schema, onArgs?)` | runs every startup check of `withValidation` without wrapping anything; returns the `@constraint` directive, or `undefined` when the schema declares none |
|
|
243
|
+
| `checkConstraints(schema, onArgs?, formats?)` | runs every startup check of `withValidation` without wrapping anything; returns the `@constraint` directive, or `undefined` when the schema declares none. `formats` is the record `withValidation` takes, checked the same way |
|
|
188
244
|
| `constraintsOn(directive, node)` | the `Constraint`s written on an argument or input field (`arg.astNode`), `format` first |
|
|
189
|
-
| `inputCode(type, constraints, where, named, options?)` | the schema of that argument or field as source, `z` a free identifier: built-in scalars and lists written in full, nullable types `.nullish()`; every other named type (enum, input object, custom scalar) is what `named(type)` returns. A constraint that cannot apply throws the startup message, naming `where`. `options` (`InputCodeOptions`): `list({ type, code, single })` returns each list's source, given `code` (the array with every rule) and `single` (the list's named type, non-null, without rules), e.g. to take a single value as graphql does: `single`, wrapped, piped into `code` |
|
|
245
|
+
| `inputCode(type, constraints, where, named, options?, formats?)` | the schema of that argument or field as source, `z` a free identifier: built-in scalars and lists written in full, nullable types `.nullish()`; every other named type (enum, input object, custom scalar) is what `named(type)` returns. A constraint that cannot apply throws the startup message, naming `where`. `options` (`InputCodeOptions`): `list({ type, code, single })` returns each list's source, given `code` (the array with every rule) and `single` (the list's named type, non-null, without rules), e.g. to take a single value as graphql does: `single`, wrapped, piped into `code`. `format(name)` returns the source of one of your formats, since only the generator knows where your record is imported from: a reference to your schema, which the other rules narrow (`formatSchemas.siret.max(14)`); built-in formats never reach it. `formats`: the record `withValidation` takes, checked the same way, so its names are known; a field using one of them without the `format` option throws `@constraint(format: "<name>") is one of the application's formats: its source is the code generator's to write, through inputCode's format option.` |
|
|
246
|
+
| `FormatSchemas` | the type of that record |
|
|
190
247
|
| `Constraint` | `{ rule, value }`, one `@constraint` argument. Of `rule`, read `argument` (`'minItems'`, `'format'`, …), `target` (`'list'` for `minItems`/`maxItems`, else what the rule narrows) and `base` (`true` for `format`, which picks the schema the others narrow); the rest is internal |
|
|
191
248
|
|
|
192
249
|
```ts
|
|
@@ -233,4 +290,4 @@ Every startup error and its fix is in [Troubleshooting](docs/troubleshooting.md)
|
|
|
233
290
|
- [Documentation index](docs/README.md)
|
|
234
291
|
- Guides: [Constraints](docs/guide/constraints.md), [Errors](docs/guide/errors.md)
|
|
235
292
|
- [Troubleshooting](docs/troubleshooting.md)
|
|
236
|
-
- [Roadmap](docs/roadmap.md):
|
|
293
|
+
- [Roadmap](docs/roadmap.md): your own formats in the Zod codegen plugin, schemas that read the context
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type GraphQLDirective, type GraphQLField, type GraphQLObjectType, type GraphQLSchema } from 'graphql';
|
|
2
2
|
import type { z } from 'zod';
|
|
3
|
+
import { type FormatSchemas } from '../formats/registry.js';
|
|
3
4
|
/**
|
|
4
5
|
* Runs every check `withValidation` makes at startup, so a code generator
|
|
5
6
|
* refuses the same schemas: the declaration is `constraintTypeDefs`', every
|
|
@@ -8,8 +9,12 @@ import type { z } from 'zod';
|
|
|
8
9
|
* default value breaks its own. `onArgs` receives each object field's
|
|
9
10
|
* arguments schema, for a field with something to check.
|
|
10
11
|
*
|
|
12
|
+
* `formats` are the application's own, which `@constraint(format: "...")`
|
|
13
|
+
* may name beside the built-in ones; they are checked first (name, no
|
|
14
|
+
* built-in replaced, a Zod string schema each).
|
|
15
|
+
*
|
|
11
16
|
* Returns the `@constraint` directive, or `undefined` when the schema
|
|
12
17
|
* declares none (and so carries no constraint).
|
|
13
18
|
*/
|
|
14
|
-
export declare function checkConstraints(schema: GraphQLSchema, onArgs?: (type: GraphQLObjectType, field: GraphQLField<unknown, unknown>, args: z.ZodType) => void): GraphQLDirective | undefined;
|
|
19
|
+
export declare function checkConstraints(schema: GraphQLSchema, onArgs?: (type: GraphQLObjectType, field: GraphQLField<unknown, unknown>, args: z.ZodType) => void, formats?: FormatSchemas): GraphQLDirective | undefined;
|
|
15
20
|
//# sourceMappingURL=check-constraints.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-constraints.d.ts","sourceRoot":"","sources":["../../src/builder/check-constraints.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAGlB,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"check-constraints.d.ts","sourceRoot":"","sources":["../../src/builder/check-constraints.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAGlB,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,EAAE,KAAK,aAAa,EAAc,MAAM,qBAAqB,CAAC;AAKrE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,aAAa,EACrB,MAAM,CAAC,EAAE,CACR,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,EACrC,IAAI,EAAE,CAAC,CAAC,OAAO,KACX,IAAI,EACT,OAAO,CAAC,EAAE,aAAa,GACrB,gBAAgB,GAAG,SAAS,CAyB9B"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type GraphQLInputType, type GraphQLList, type GraphQLNamedInputType } from 'graphql';
|
|
2
|
+
import { type FormatSchemas } from '../formats/registry.js';
|
|
2
3
|
import type { Constraint } from './constraints.js';
|
|
3
4
|
/**
|
|
4
5
|
* The source of the schema of one argument or input field: what
|
|
@@ -10,8 +11,12 @@ import type { Constraint } from './constraints.js';
|
|
|
10
11
|
* object's or an enum's schema by name, a custom scalar's from a mapping.
|
|
11
12
|
* The built-in scalars are written here, with their constraints. A
|
|
12
13
|
* constraint that cannot apply fails as it does at startup.
|
|
14
|
+
*
|
|
15
|
+
* `formats` are the application's own, as `withValidation` takes them: a
|
|
16
|
+
* name among them is a known format, which a built-in one is not. Their
|
|
17
|
+
* source is `options.format`'s to write.
|
|
13
18
|
*/
|
|
14
|
-
export declare function inputCode(type: GraphQLInputType, constraints: readonly Constraint[], where: string, named: (type: GraphQLNamedInputType) => string, options?: InputCodeOptions): string;
|
|
19
|
+
export declare function inputCode(type: GraphQLInputType, constraints: readonly Constraint[], where: string, named: (type: GraphQLNamedInputType) => string, options?: InputCodeOptions, formats?: FormatSchemas): string;
|
|
15
20
|
/** How `inputCode` writes what a generator may want written otherwise. */
|
|
16
21
|
export interface InputCodeOptions {
|
|
17
22
|
/**
|
|
@@ -25,5 +30,13 @@ export interface InputCodeOptions {
|
|
|
25
30
|
readonly code: string;
|
|
26
31
|
readonly single: string;
|
|
27
32
|
}) => string;
|
|
33
|
+
/**
|
|
34
|
+
* The source of one of the application's formats, given its name (a key
|
|
35
|
+
* of the `formats` record): a reference to the application's own schema,
|
|
36
|
+
* which the rules after `format` narrow (`formatSchemas.slug.max(40)`).
|
|
37
|
+
* A built-in format is written inline and never reaches it. Without it,
|
|
38
|
+
* a field using one of the application's formats throws.
|
|
39
|
+
*/
|
|
40
|
+
readonly format?: (name: string) => string;
|
|
28
41
|
}
|
|
29
42
|
//# sourceMappingURL=input-code.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input-code.d.ts","sourceRoot":"","sources":["../../src/builder/input-code.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAK1B,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"input-code.d.ts","sourceRoot":"","sources":["../../src/builder/input-code.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAK1B,MAAM,SAAS,CAAC;AACjB,OAAO,EAEN,KAAK,aAAa,EAElB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CACxB,IAAI,EAAE,gBAAgB,EACtB,WAAW,EAAE,SAAS,UAAU,EAAE,EAClC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,MAAM,EAC9C,OAAO,GAAE,gBAAqB,EAC9B,OAAO,CAAC,EAAE,aAAa,GACrB,MAAM,CAQR;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE;QACtB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;QAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACxB,KAAK,MAAM,CAAC;IACb;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3C"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type GraphQLArgument, type GraphQLDirective, type GraphQLInputField, type GraphQLInputType, type GraphQLSchema } from 'graphql';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { type FormatRegistry } from '../formats/registry.js';
|
|
3
4
|
/**
|
|
4
5
|
* Builds the schemas of the arguments and input fields of one GraphQL
|
|
5
6
|
* schema, once each: an input type is walked a single time however many
|
|
@@ -9,7 +10,8 @@ import { z } from 'zod';
|
|
|
9
10
|
export declare class InputSchemas {
|
|
10
11
|
#private;
|
|
11
12
|
readonly directive: GraphQLDirective | undefined;
|
|
12
|
-
|
|
13
|
+
/** `formats`: the built-in ones by default, the application's added. */
|
|
14
|
+
constructor(schema: GraphQLSchema, formats?: FormatRegistry);
|
|
13
15
|
/** Whether a value of this type can break a constraint anywhere inside. */
|
|
14
16
|
isConstrained(type: GraphQLInputType): boolean;
|
|
15
17
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input-schema.d.ts","sourceRoot":"","sources":["../../src/builder/input-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAEtB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAMlB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"input-schema.d.ts","sourceRoot":"","sources":["../../src/builder/input-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAEtB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAMlB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,qBAAqB,CAAC;AAMtE;;;;;GAKG;AACH,qBAAa,YAAY;;IACxB,QAAQ,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAYjD,wEAAwE;gBAC5D,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,cAA6B;IAyBzE,2EAA2E;IAC3E,aAAa,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO;IAK9C;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAMrC,gEAAgE;IAChE,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,iBAAiB,GAAG,OAAO;IAO/D,8EAA8E;IAC9E,EAAE,CAAC,KAAK,EAAE,eAAe,GAAG,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO;IAYxE;;;;OAIG;IACH,aAAa,IAAI,IAAI;CA+ErB"}
|
package/dist/builder/leaf.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type GraphQLLeafType } from 'graphql';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import type { Target } from '../rules/rule.js';
|
|
3
|
+
import type { RuleContext, Target } from '../rules/rule.js';
|
|
4
4
|
import type { Constraint } from './constraints.js';
|
|
5
5
|
/**
|
|
6
6
|
* Fails unless every constraint applies to a value of `type`: `minLength` on
|
|
@@ -16,7 +16,7 @@ export declare function assertObjectTargets(typeName: string, constraints: reado
|
|
|
16
16
|
* without any is left as graphql parsed it. A constraint that cannot apply
|
|
17
17
|
* fails here, at startup.
|
|
18
18
|
*/
|
|
19
|
-
export declare function leafSchema(type: GraphQLLeafType, constraints: readonly Constraint[], where: string): z.ZodType;
|
|
19
|
+
export declare function leafSchema(type: GraphQLLeafType, constraints: readonly Constraint[], where: string, context: RuleContext): z.ZodType;
|
|
20
20
|
/** The source of a built-in scalar's schema, before its constraints. */
|
|
21
21
|
export declare function baseCode(type: GraphQLLeafType): string | undefined;
|
|
22
22
|
/** What a rule's target reads as in an error. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"leaf.d.ts","sourceRoot":"","sources":["../../src/builder/leaf.ts"],"names":[],"mappings":"AAAA,OAAO,EAKN,KAAK,eAAe,EAEpB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"leaf.d.ts","sourceRoot":"","sources":["../../src/builder/leaf.ts"],"names":[],"mappings":"AAAA,OAAO,EAKN,KAAK,eAAe,EAEpB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAehD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,eAAe,EACrB,WAAW,EAAE,SAAS,UAAU,EAAE,EAClC,KAAK,EAAE,MAAM,GACX,IAAI,CAMN;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,SAAS,UAAU,EAAE,EAClC,KAAK,EAAE,MAAM,GACX,IAAI,CAIN;AAYD;;;;GAIG;AACH,wBAAgB,UAAU,CACzB,IAAI,EAAE,eAAe,EACrB,WAAW,EAAE,SAAS,UAAU,EAAE,EAClC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,WAAW,GAClB,CAAC,CAAC,OAAO,CAQX;AAED,wEAAwE;AACxE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAMlE;AAED,iDAAiD;AACjD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIrD"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
__export,
|
|
3
|
+
RULE_PARAM,
|
|
2
4
|
rules,
|
|
3
5
|
applyRule,
|
|
4
6
|
assertOwnConstraint
|
|
5
|
-
} from "./index-
|
|
7
|
+
} from "./index-xcaf9kes.js";
|
|
6
8
|
|
|
7
9
|
// src/builder/constraints.ts
|
|
8
10
|
import {
|
|
@@ -24,12 +26,196 @@ import {
|
|
|
24
26
|
isObjectType
|
|
25
27
|
} from "graphql";
|
|
26
28
|
|
|
27
|
-
// src/
|
|
29
|
+
// src/formats/registry.ts
|
|
30
|
+
import { z as z9 } from "zod";
|
|
31
|
+
|
|
32
|
+
// src/formats/all.ts
|
|
33
|
+
var exports_all = {};
|
|
34
|
+
__export(exports_all, {
|
|
35
|
+
byteFormat: () => byteFormat,
|
|
36
|
+
dateFormat: () => dateFormat,
|
|
37
|
+
dateTimeFormat: () => dateTimeFormat,
|
|
38
|
+
emailFormat: () => emailFormat,
|
|
39
|
+
ipv4Format: () => ipv4Format,
|
|
40
|
+
ipv6Format: () => ipv6Format,
|
|
41
|
+
uriFormat: () => uriFormat,
|
|
42
|
+
uuidFormat: () => uuidFormat
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// src/formats/byte.ts
|
|
28
46
|
import { z } from "zod";
|
|
47
|
+
|
|
48
|
+
// src/formats/format.ts
|
|
49
|
+
function defineFormat(format) {
|
|
50
|
+
return format;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/formats/byte.ts
|
|
54
|
+
var byteFormat = defineFormat({
|
|
55
|
+
name: "byte",
|
|
56
|
+
toZod: () => z.base64(),
|
|
57
|
+
toCode: () => "z.base64()"
|
|
58
|
+
});
|
|
59
|
+
// src/formats/date.ts
|
|
60
|
+
import { z as z2 } from "zod";
|
|
61
|
+
var dateFormat = defineFormat({
|
|
62
|
+
name: "date",
|
|
63
|
+
toZod: () => z2.iso.date(),
|
|
64
|
+
toCode: () => "z.iso.date()"
|
|
65
|
+
});
|
|
66
|
+
// src/formats/date-time.ts
|
|
67
|
+
import { z as z3 } from "zod";
|
|
68
|
+
var dateTimeFormat = defineFormat({
|
|
69
|
+
name: "date-time",
|
|
70
|
+
toZod: () => z3.iso.datetime({ offset: true }),
|
|
71
|
+
toCode: () => "z.iso.datetime({ offset: true })"
|
|
72
|
+
});
|
|
73
|
+
// src/formats/email.ts
|
|
74
|
+
import { z as z4 } from "zod";
|
|
75
|
+
var emailFormat = defineFormat({
|
|
76
|
+
name: "email",
|
|
77
|
+
toZod: () => z4.email(),
|
|
78
|
+
toCode: () => "z.email()"
|
|
79
|
+
});
|
|
80
|
+
// src/formats/ipv4.ts
|
|
81
|
+
import { z as z5 } from "zod";
|
|
82
|
+
var ipv4Format = defineFormat({
|
|
83
|
+
name: "ipv4",
|
|
84
|
+
toZod: () => z5.ipv4(),
|
|
85
|
+
toCode: () => "z.ipv4()"
|
|
86
|
+
});
|
|
87
|
+
// src/formats/ipv6.ts
|
|
88
|
+
import { z as z6 } from "zod";
|
|
89
|
+
var ipv6Format = defineFormat({
|
|
90
|
+
name: "ipv6",
|
|
91
|
+
toZod: () => z6.ipv6(),
|
|
92
|
+
toCode: () => "z.ipv6()"
|
|
93
|
+
});
|
|
94
|
+
// src/formats/uri.ts
|
|
95
|
+
import { z as z7 } from "zod";
|
|
96
|
+
var uriFormat = defineFormat({
|
|
97
|
+
name: "uri",
|
|
98
|
+
toZod: () => z7.url({ protocol: /^(https?|ftp)$/ }),
|
|
99
|
+
toCode: () => "z.url({ protocol: /^(https?|ftp)$/ })"
|
|
100
|
+
});
|
|
101
|
+
// src/formats/uuid.ts
|
|
102
|
+
import { z as z8 } from "zod";
|
|
103
|
+
var uuidFormat = defineFormat({
|
|
104
|
+
name: "uuid",
|
|
105
|
+
toZod: () => z8.uuid(),
|
|
106
|
+
toCode: () => "z.uuid()"
|
|
107
|
+
});
|
|
108
|
+
// src/formats/index.ts
|
|
109
|
+
var formats = Object.fromEntries(Object.values(exports_all).map((format) => [format.name, format]));
|
|
110
|
+
|
|
111
|
+
// src/formats/registry.ts
|
|
112
|
+
var NAME = /^[a-z][a-z0-9-]*$/;
|
|
113
|
+
|
|
114
|
+
class FormatRegistry {
|
|
115
|
+
#formats = new Map;
|
|
116
|
+
#async;
|
|
117
|
+
constructor(own = {}) {
|
|
118
|
+
for (const format of Object.values(formats))
|
|
119
|
+
this.#formats.set(format.name, format);
|
|
120
|
+
for (const [name, schema] of Object.entries(own)) {
|
|
121
|
+
if (!NAME.test(name)) {
|
|
122
|
+
throw new Error(`Invalid format name ${JSON.stringify(name)}: write it in lowercase letters, digits and hyphens, starting with a letter.`);
|
|
123
|
+
}
|
|
124
|
+
if (this.#formats.has(name)) {
|
|
125
|
+
throw new Error(`The format "${name}" is built in: give yours another name.`);
|
|
126
|
+
}
|
|
127
|
+
if (!isStringSchema(schema)) {
|
|
128
|
+
throw new Error(`The format "${name}" is not a Zod string schema: write z.string() or a string format such as z.email(), narrowed with .regex() or .refine(), never transformed.`);
|
|
129
|
+
}
|
|
130
|
+
if (!runnable(schema)) {
|
|
131
|
+
throw new Error(`The format "${name}" cannot be run: its definition reads type: 'string' but it has no _zod.run, so it was not built by zod 4. Pass the schema z.string() or a string format returns, not an object shaped like one.`);
|
|
132
|
+
}
|
|
133
|
+
if (rewrites(schema)) {
|
|
134
|
+
throw new Error(`The format "${name}" rewrites the value (.trim(), .toLowerCase(), .toUpperCase(), .normalize(), .slugify(), .overwrite(), z.url(), z.httpUrl() or z.coerce.string()): refuse what is not canonical with .regex() or .refine() instead, so the server and the client check the value as it was sent.`);
|
|
135
|
+
}
|
|
136
|
+
const own = marked(name, schema, () => {
|
|
137
|
+
this.#async = name;
|
|
138
|
+
});
|
|
139
|
+
this.#formats.set(name, { name, toZod: () => own });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
named(name) {
|
|
143
|
+
const format = this.#formats.get(name);
|
|
144
|
+
if (format)
|
|
145
|
+
return format;
|
|
146
|
+
throw new Error(`Unknown @constraint format "${name}". Known formats: ${[...this.#formats.keys()].join(", ")}.`);
|
|
147
|
+
}
|
|
148
|
+
takeAsync() {
|
|
149
|
+
const name = this.#async;
|
|
150
|
+
this.#async = undefined;
|
|
151
|
+
return name;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function isStringSchema(value) {
|
|
155
|
+
return definition(value)?.type === "string";
|
|
156
|
+
}
|
|
157
|
+
function runnable(schema) {
|
|
158
|
+
return typeof schema?._zod?.run === "function";
|
|
159
|
+
}
|
|
160
|
+
function rewrites(schema) {
|
|
161
|
+
const def = definition(schema);
|
|
162
|
+
if (def?.coerce || def?.format === "url")
|
|
163
|
+
return true;
|
|
164
|
+
return (def?.checks ?? []).some((check) => {
|
|
165
|
+
const own = definition(check);
|
|
166
|
+
return own?.check === "overwrite" || own?.format === "url";
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function definition(value) {
|
|
170
|
+
return value?._zod?.def;
|
|
171
|
+
}
|
|
172
|
+
function marked(name, schema, onAsync) {
|
|
173
|
+
return z9.string().superRefine((value, ctx) => {
|
|
174
|
+
const raise = (result) => {
|
|
175
|
+
if (result.issues.length === 0 && result.value !== value) {
|
|
176
|
+
throw new Error(`The format "${name}" rewrote the value it checked: a format checks the value and never changes it, so the server and the client check the value as it was sent. Refuse what is not canonical with .regex() or .refine() instead of setting payload.value in a .check().`);
|
|
177
|
+
}
|
|
178
|
+
for (const raw of result.issues) {
|
|
179
|
+
const issue = z9.core.util.finalizeIssue(raw, RUN, z9.core.config());
|
|
180
|
+
const params = "params" in issue ? issue.params : undefined;
|
|
181
|
+
ctx.addIssue({
|
|
182
|
+
...issue,
|
|
183
|
+
params: { ...params, [RULE_PARAM]: "format" },
|
|
184
|
+
...raw.continue === false && { continue: false }
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const result = schema._zod.run({ value, issues: [] }, RUN);
|
|
189
|
+
if (!(result instanceof Promise)) {
|
|
190
|
+
raise(result);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const pending = result.then(raise);
|
|
194
|
+
pending.catch(() => {});
|
|
195
|
+
onAsync();
|
|
196
|
+
return pending;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
var RUN = { async: true };
|
|
200
|
+
var builtInRegistry = new FormatRegistry;
|
|
201
|
+
var registries = new WeakMap;
|
|
202
|
+
function registryOf(own) {
|
|
203
|
+
if (!own)
|
|
204
|
+
return builtInRegistry;
|
|
205
|
+
let registry = registries.get(own);
|
|
206
|
+
if (!registry) {
|
|
207
|
+
registry = new FormatRegistry(own);
|
|
208
|
+
registries.set(own, registry);
|
|
209
|
+
}
|
|
210
|
+
return registry;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/builder/args-schema.ts
|
|
214
|
+
import { z as z10 } from "zod";
|
|
29
215
|
function argsSchemaOf(inputs, parent, field) {
|
|
30
216
|
if (!field.args.some((arg) => inputs.needsCheck(arg)))
|
|
31
217
|
return;
|
|
32
|
-
return
|
|
218
|
+
return z10.object(Object.fromEntries(field.args.map((arg) => [
|
|
33
219
|
arg.name,
|
|
34
220
|
inputs.of(arg, `${parent}.${field.name}(${arg.name}:)`)
|
|
35
221
|
])));
|
|
@@ -65,7 +251,7 @@ import {
|
|
|
65
251
|
isNonNullType,
|
|
66
252
|
valueFromAST
|
|
67
253
|
} from "graphql";
|
|
68
|
-
import { z as
|
|
254
|
+
import { z as z12 } from "zod";
|
|
69
255
|
|
|
70
256
|
// src/builder/leaf.ts
|
|
71
257
|
import {
|
|
@@ -75,7 +261,7 @@ import {
|
|
|
75
261
|
GraphQLInt,
|
|
76
262
|
GraphQLString
|
|
77
263
|
} from "graphql";
|
|
78
|
-
import { z as
|
|
264
|
+
import { z as z11 } from "zod";
|
|
79
265
|
var kinds = {
|
|
80
266
|
[GraphQLString.name]: "string",
|
|
81
267
|
[GraphQLID.name]: "string",
|
|
@@ -84,8 +270,8 @@ var kinds = {
|
|
|
84
270
|
};
|
|
85
271
|
function baseOf(type) {
|
|
86
272
|
if (type.name === GraphQLInt.name)
|
|
87
|
-
return
|
|
88
|
-
return kinds[type.name] === "number" ?
|
|
273
|
+
return z11.number().int();
|
|
274
|
+
return kinds[type.name] === "number" ? z11.number() : z11.string();
|
|
89
275
|
}
|
|
90
276
|
function assertLeafTargets(type, constraints, where) {
|
|
91
277
|
const misplaced = constraints.find(({ rule }) => rule.target === "list");
|
|
@@ -104,13 +290,13 @@ function assertObjectTargets(typeName, constraints, where) {
|
|
|
104
290
|
function misplacedOn({ rule }, where, typeName) {
|
|
105
291
|
return new Error(`@constraint(${rule.argument}) on ${where} needs ${describeTarget(rule.target)}, not ${typeName}.`);
|
|
106
292
|
}
|
|
107
|
-
function leafSchema(type, constraints, where) {
|
|
293
|
+
function leafSchema(type, constraints, where, context) {
|
|
108
294
|
if (constraints.length === 0)
|
|
109
|
-
return
|
|
295
|
+
return z11.unknown();
|
|
110
296
|
assertLeafTargets(type, constraints, where);
|
|
111
297
|
let schema = baseOf(type);
|
|
112
298
|
for (const { rule, value } of constraints) {
|
|
113
|
-
schema = applyRule(rule, schema, value);
|
|
299
|
+
schema = applyRule(rule, schema, value, context);
|
|
114
300
|
}
|
|
115
301
|
return schema;
|
|
116
302
|
}
|
|
@@ -134,10 +320,12 @@ function describeTarget(target) {
|
|
|
134
320
|
// src/builder/input-schema.ts
|
|
135
321
|
class InputSchemas {
|
|
136
322
|
directive;
|
|
323
|
+
#context;
|
|
137
324
|
#objects = new Map;
|
|
138
325
|
#defaults = [];
|
|
139
326
|
#constrained = new Set;
|
|
140
|
-
constructor(schema) {
|
|
327
|
+
constructor(schema, formats = registryOf()) {
|
|
328
|
+
this.#context = { formats };
|
|
141
329
|
this.directive = schema.getDirective("constraint") ?? undefined;
|
|
142
330
|
const objects = Object.values(schema.getTypeMap()).filter(isInputObjectType);
|
|
143
331
|
for (let changed = true;changed; ) {
|
|
@@ -178,7 +366,15 @@ class InputSchemas {
|
|
|
178
366
|
const value = valueFromAST(literal, type);
|
|
179
367
|
if (value === undefined)
|
|
180
368
|
continue;
|
|
181
|
-
|
|
369
|
+
let result;
|
|
370
|
+
try {
|
|
371
|
+
result = schema.safeParse(value);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (!(error instanceof z12.core.$ZodAsyncError))
|
|
374
|
+
throw error;
|
|
375
|
+
const format = this.#context.formats.takeAsync();
|
|
376
|
+
throw new Error(`The default value of ${where} cannot be checked at startup: ${format ? `the format "${format}"` : "its format"} checks asynchronously, and a default value is checked synchronously. Drop the default, or move the async check out of the format into validated().`);
|
|
377
|
+
}
|
|
182
378
|
if (!result.success) {
|
|
183
379
|
throw new Error(`The default value of ${where} breaks its @constraint: ${result.error.issues[0]?.message}`);
|
|
184
380
|
}
|
|
@@ -195,26 +391,26 @@ class InputSchemas {
|
|
|
195
391
|
if (isListType(type)) {
|
|
196
392
|
const own = constraints.filter(({ rule }) => rule.target === "list");
|
|
197
393
|
const items = constraints.filter(({ rule }) => rule.target !== "list");
|
|
198
|
-
let list =
|
|
394
|
+
let list = z12.array(this.#typed(type.ofType, items, where));
|
|
199
395
|
for (const { rule, value } of own)
|
|
200
|
-
list = applyRule(rule, list, value);
|
|
396
|
+
list = applyRule(rule, list, value, this.#context);
|
|
201
397
|
return list;
|
|
202
398
|
}
|
|
203
399
|
if (isInputObjectType(type)) {
|
|
204
400
|
assertObjectTargets(type.name, constraints, where);
|
|
205
401
|
return this.#object(type);
|
|
206
402
|
}
|
|
207
|
-
return leafSchema(type, constraints, where);
|
|
403
|
+
return leafSchema(type, constraints, where, this.#context);
|
|
208
404
|
}
|
|
209
405
|
#object(type) {
|
|
210
406
|
if (!this.isConstrained(type))
|
|
211
|
-
return
|
|
407
|
+
return z12.unknown();
|
|
212
408
|
const built = this.#objects.get(type.name);
|
|
213
409
|
if (built)
|
|
214
410
|
return built;
|
|
215
411
|
let schema;
|
|
216
|
-
this.#objects.set(type.name,
|
|
217
|
-
schema =
|
|
412
|
+
this.#objects.set(type.name, z12.lazy(() => schema));
|
|
413
|
+
schema = z12.object(Object.fromEntries(Object.values(type.getFields()).map((field) => [
|
|
218
414
|
field.name,
|
|
219
415
|
this.of(field, `${type.name}.${field.name}`)
|
|
220
416
|
])));
|
|
@@ -224,8 +420,8 @@ class InputSchemas {
|
|
|
224
420
|
}
|
|
225
421
|
|
|
226
422
|
// src/builder/check-constraints.ts
|
|
227
|
-
function checkConstraints2(schema, onArgs) {
|
|
228
|
-
const inputs = new InputSchemas(schema);
|
|
423
|
+
function checkConstraints2(schema, onArgs, formats) {
|
|
424
|
+
const inputs = new InputSchemas(schema, registryOf(formats));
|
|
229
425
|
if (!inputs.directive)
|
|
230
426
|
return;
|
|
231
427
|
assertOwnConstraint(inputs.directive);
|
|
@@ -260,7 +456,7 @@ function assertNoDirectiveArgumentConstrained(inputs, schema) {
|
|
|
260
456
|
}
|
|
261
457
|
}
|
|
262
458
|
|
|
263
|
-
export { constraintsOn2, assertLeafTargets, assertObjectTargets, baseCode, checkConstraints2 };
|
|
459
|
+
export { registryOf, constraintsOn2, assertLeafTargets, assertObjectTargets, baseCode, checkConstraints2 };
|
|
264
460
|
|
|
265
|
-
//# debugId=
|
|
266
|
-
//# sourceMappingURL=index-
|
|
461
|
+
//# debugId=3026B70F7346E03264756E2164756E21
|
|
462
|
+
//# sourceMappingURL=index-1mzzydy3.js.map
|