@nxgt/graphql-validation 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +235 -2
- package/dist/bad-user-input.d.ts +35 -0
- package/dist/bad-user-input.d.ts.map +1 -0
- package/dist/builder/args-schema.d.ts +17 -0
- package/dist/builder/args-schema.d.ts.map +1 -0
- package/dist/builder/check-constraints.d.ts +15 -0
- package/dist/builder/check-constraints.d.ts.map +1 -0
- package/dist/builder/constraints.d.ts +16 -0
- package/dist/builder/constraints.d.ts.map +1 -0
- package/dist/builder/input-code.d.ts +29 -0
- package/dist/builder/input-code.d.ts.map +1 -0
- package/dist/builder/input-schema.d.ts +31 -0
- package/dist/builder/input-schema.d.ts.map +1 -0
- package/dist/builder/leaf.d.ts +24 -0
- package/dist/builder/leaf.d.ts.map +1 -0
- package/dist/chunks/index-rbxe5m3y.js +319 -0
- package/dist/chunks/index-rbxe5m3y.js.map +37 -0
- package/dist/chunks/index-s1kkmg35.js +266 -0
- package/dist/chunks/index-s1kkmg35.js.map +14 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +69 -0
- package/dist/cli.js.map +11 -0
- package/dist/codegen.d.ts +4 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +56 -0
- package/dist/codegen.js.map +10 -0
- package/dist/constraint-directive.d.ts +21 -0
- package/dist/constraint-directive.d.ts.map +1 -0
- package/dist/formats/all.d.ts +9 -0
- package/dist/formats/all.d.ts.map +1 -0
- package/dist/formats/byte.d.ts +2 -0
- package/dist/formats/byte.d.ts.map +1 -0
- package/dist/formats/date-time.d.ts +2 -0
- package/dist/formats/date-time.d.ts.map +1 -0
- package/dist/formats/date.d.ts +2 -0
- package/dist/formats/date.d.ts.map +1 -0
- package/dist/formats/email.d.ts +2 -0
- package/dist/formats/email.d.ts.map +1 -0
- package/dist/formats/format.d.ts +15 -0
- package/dist/formats/format.d.ts.map +1 -0
- package/dist/formats/index.d.ts +13 -0
- package/dist/formats/index.d.ts.map +1 -0
- package/dist/formats/ipv4.d.ts +2 -0
- package/dist/formats/ipv4.d.ts.map +1 -0
- package/dist/formats/ipv6.d.ts +2 -0
- package/dist/formats/ipv6.d.ts.map +1 -0
- package/dist/formats/uri.d.ts +2 -0
- package/dist/formats/uri.d.ts.map +1 -0
- package/dist/formats/uuid.d.ts +2 -0
- package/dist/formats/uuid.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +12 -0
- package/dist/rules/all.d.ts +16 -0
- package/dist/rules/all.d.ts.map +1 -0
- package/dist/rules/contains.d.ts +2 -0
- package/dist/rules/contains.d.ts.map +1 -0
- package/dist/rules/ends-with.d.ts +2 -0
- package/dist/rules/ends-with.d.ts.map +1 -0
- package/dist/rules/exclusive-max.d.ts +2 -0
- package/dist/rules/exclusive-max.d.ts.map +1 -0
- package/dist/rules/exclusive-min.d.ts +2 -0
- package/dist/rules/exclusive-min.d.ts.map +1 -0
- package/dist/rules/format.d.ts +2 -0
- package/dist/rules/format.d.ts.map +1 -0
- package/dist/rules/index.d.ts +28 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/max-items.d.ts +2 -0
- package/dist/rules/max-items.d.ts.map +1 -0
- package/dist/rules/max-length.d.ts +2 -0
- package/dist/rules/max-length.d.ts.map +1 -0
- package/dist/rules/max.d.ts +2 -0
- package/dist/rules/max.d.ts.map +1 -0
- package/dist/rules/min-items.d.ts +2 -0
- package/dist/rules/min-items.d.ts.map +1 -0
- package/dist/rules/min-length.d.ts +2 -0
- package/dist/rules/min-length.d.ts.map +1 -0
- package/dist/rules/min.d.ts +2 -0
- package/dist/rules/min.d.ts.map +1 -0
- package/dist/rules/multiple-of.d.ts +2 -0
- package/dist/rules/multiple-of.d.ts.map +1 -0
- package/dist/rules/not-contains.d.ts +2 -0
- package/dist/rules/not-contains.d.ts.map +1 -0
- package/dist/rules/pattern.d.ts +2 -0
- package/dist/rules/pattern.d.ts.map +1 -0
- package/dist/rules/rule.d.ts +55 -0
- package/dist/rules/rule.d.ts.map +1 -0
- package/dist/rules/starts-with.d.ts +2 -0
- package/dist/rules/starts-with.d.ts.map +1 -0
- package/dist/typedefs-command.d.ts +10 -0
- package/dist/typedefs-command.d.ts.map +1 -0
- package/dist/validated.d.ts +22 -0
- package/dist/validated.d.ts.map +1 -0
- package/dist/with-validation.d.ts +26 -0
- package/dist/with-validation.d.ts.map +1 -0
- package/docs/README.md +15 -0
- package/docs/guide/constraints.md +291 -0
- package/docs/guide/errors.md +194 -0
- package/docs/roadmap.md +31 -0
- package/docs/troubleshooting.md +260 -0
- package/graphql/constraint.graphqls +20 -0
- package/package.json +73 -5
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
The error an invalid input becomes, how a client reads it, and `validated` for
|
|
4
|
+
rules a `@constraint` cannot say. For declaring constraints, see
|
|
5
|
+
[Constraints](constraints.md).
|
|
6
|
+
|
|
7
|
+
## The error
|
|
8
|
+
|
|
9
|
+
An invalid input is **one** `GraphQLError`, thrown before your resolver runs:
|
|
10
|
+
|
|
11
|
+
- `message`: `Invalid arguments for <Type>.<field>. <path>: <first issue message>`,
|
|
12
|
+
for a log line. The path is dotted; `arguments` stands for an empty path.
|
|
13
|
+
- `extensions.code`: `BAD_USER_INPUT`, the code Apollo and most clients read.
|
|
14
|
+
- `extensions.issues`: every reason, not only the first.
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"errors": [
|
|
19
|
+
{
|
|
20
|
+
"message": "Invalid arguments for Mutation.signUp. input.email: Invalid email address",
|
|
21
|
+
"path": ["signUp"],
|
|
22
|
+
"extensions": {
|
|
23
|
+
"code": "BAD_USER_INPUT",
|
|
24
|
+
"issues": [
|
|
25
|
+
{ "path": ["input", "email"], "message": "Invalid email address", "code": "invalid_format", "constraint": "format" },
|
|
26
|
+
{ "path": ["input", "name"], "message": "Too small: expected string to have >=2 characters", "code": "too_small", "constraint": "minLength" },
|
|
27
|
+
{ "path": ["input", "tags"], "message": "Too big: expected array to have <=2 items", "code": "too_big", "constraint": "maxItems" }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"data": { "signUp": null }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The error's own `path` is the field; each issue's `path` is **relative to the
|
|
37
|
+
arguments**: the argument name, then input fields, with a number for a list
|
|
38
|
+
index. `code` is Zod's issue code (`too_small`, `too_big`, `invalid_format`,
|
|
39
|
+
`custom`, ...). `constraint` is the `@constraint` argument that refused
|
|
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
|
|
42
|
+
meant for display in every language: map on `constraint` and `path` if you
|
|
43
|
+
translate. Prefer `constraint` to `code`: it is the name in your schema, while
|
|
44
|
+
`code` is Zod's and can change when Zod does across a major version.
|
|
45
|
+
|
|
46
|
+
### Types
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
interface ValidationIssue {
|
|
50
|
+
readonly path: readonly (string | number)[];
|
|
51
|
+
readonly message: string;
|
|
52
|
+
readonly code: string;
|
|
53
|
+
/** The `@constraint` argument that refused; absent for `validated`'s own schema. */
|
|
54
|
+
readonly constraint?: ConstraintArgument;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type ConstraintArgument =
|
|
58
|
+
| 'format' | 'minLength' | 'maxLength' | 'startsWith' | 'endsWith'
|
|
59
|
+
| 'contains' | 'notContains' | 'pattern' | 'min' | 'max'
|
|
60
|
+
| 'exclusiveMin' | 'exclusiveMax' | 'multipleOf' | 'minItems' | 'maxItems';
|
|
61
|
+
|
|
62
|
+
type BadUserInputExtensions = {
|
|
63
|
+
readonly code: 'BAD_USER_INPUT';
|
|
64
|
+
readonly issues: readonly ValidationIssue[];
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## In a client
|
|
69
|
+
|
|
70
|
+
Map the issues onto form fields by path. A path of `['input', 'address', 'zip']`
|
|
71
|
+
is the field `address.zip` of the `input` argument.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import type { BadUserInputExtensions, ValidationIssue } from '@nxgt/graphql-validation';
|
|
75
|
+
|
|
76
|
+
interface GraphQLErrorJson {
|
|
77
|
+
message: string;
|
|
78
|
+
extensions?: Partial<BadUserInputExtensions>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Field errors keyed by `address.zip`, relative to the `input` argument. */
|
|
82
|
+
function fieldErrors(errors: readonly GraphQLErrorJson[]): Record<string, string[]> {
|
|
83
|
+
const byField: Record<string, string[]> = {};
|
|
84
|
+
for (const error of errors) {
|
|
85
|
+
if (error.extensions?.code !== 'BAD_USER_INPUT') continue;
|
|
86
|
+
for (const issue of (error.extensions.issues ?? []) as ValidationIssue[]) {
|
|
87
|
+
const [, ...field] = issue.path; // drop the argument name, `input`
|
|
88
|
+
const key = field.join('.');
|
|
89
|
+
(byField[key] ??= []).push(issue.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return byField;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// { email: ['Invalid email address'], name: ['Too small: ...'] }
|
|
96
|
+
|
|
97
|
+
/** Translated texts, keyed by the stable `constraint`, not by Zod's `code`. */
|
|
98
|
+
const texts: Record<string, string> = {
|
|
99
|
+
format: 'Not a valid value',
|
|
100
|
+
minLength: 'Too short',
|
|
101
|
+
maxItems: 'Too many items',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const textOf = (issue: ValidationIssue) =>
|
|
105
|
+
(issue.constraint && texts[issue.constraint]) ?? issue.message;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
An issue whose path is just the argument (`['code']` for `check(code: ...)`)
|
|
109
|
+
gives the empty key: show it as a form-level error.
|
|
110
|
+
|
|
111
|
+
## `validated`
|
|
112
|
+
|
|
113
|
+
`validated(schema, resolver)` checks the arguments with a Zod schema you write,
|
|
114
|
+
for what a directive cannot say: a rule across arguments or fields, a business
|
|
115
|
+
refinement, an async check. It raises the same error, so a client has one shape
|
|
116
|
+
to handle.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { validated } from '@nxgt/graphql-validation';
|
|
120
|
+
import { z } from 'zod';
|
|
121
|
+
|
|
122
|
+
const range = z
|
|
123
|
+
.object({ from: z.number(), to: z.number() })
|
|
124
|
+
.refine(({ from, to }) => from <= to, {
|
|
125
|
+
message: 'from must not exceed to',
|
|
126
|
+
path: ['to'],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const resolvers = {
|
|
130
|
+
Query: {
|
|
131
|
+
// (source, args, context, info): args is z.output of the schema
|
|
132
|
+
range: validated(range, (_, { from, to }) => to - from),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A bad `{ from: 4, to: 1 }` gives `Invalid arguments for Query.range. to: from must not exceed to`
|
|
138
|
+
with one issue `{ path: ['to'], message: 'from must not exceed to', code: 'custom' }`.
|
|
139
|
+
|
|
140
|
+
- The schema is a Zod object schema of the arguments, or its **shape**
|
|
141
|
+
(`{ name: z.string() }`, wrapped in `z.object` for you).
|
|
142
|
+
- The resolver receives `z.output`: defaults and transforms applied.
|
|
143
|
+
- Async refinements work; the check awaits them.
|
|
144
|
+
- When a field has both `@constraint` and `validated`, the directives run
|
|
145
|
+
first, then your schema.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { validated } from '@nxgt/graphql-validation';
|
|
149
|
+
import { z } from 'zod';
|
|
150
|
+
|
|
151
|
+
declare const users: { exists(name: string): Promise<boolean> };
|
|
152
|
+
|
|
153
|
+
// type Query { claim(name: String! @constraint(minLength: 3)): String }
|
|
154
|
+
const claim = validated(
|
|
155
|
+
{ name: z.string().refine(async (name) => !(await users.exists(name)), 'already taken') },
|
|
156
|
+
(_, { name }) => name,
|
|
157
|
+
);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import type { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql';
|
|
162
|
+
import type { z } from 'zod';
|
|
163
|
+
|
|
164
|
+
type ArgsSchema = z.ZodType<Record<string, unknown>> | z.ZodRawShape;
|
|
165
|
+
type SchemaOf<S extends ArgsSchema> = S extends z.ZodType ? S : z.ZodObject<S & z.ZodRawShape>;
|
|
166
|
+
|
|
167
|
+
declare function validated<S extends ArgsSchema, TSource = unknown, TContext = unknown, TResult = unknown>(
|
|
168
|
+
schema: S,
|
|
169
|
+
resolve: (source: TSource, args: z.output<SchemaOf<S>>, context: TContext, info: GraphQLResolveInfo) => TResult,
|
|
170
|
+
): GraphQLFieldResolver<TSource, TContext, z.input<SchemaOf<S>>, Promise<Awaited<TResult>>>;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`validated` validates whatever the field's arguments are; it does not need
|
|
174
|
+
`withValidation` or `constraintTypeDefs` to work.
|
|
175
|
+
|
|
176
|
+
## `badUserInput`
|
|
177
|
+
|
|
178
|
+
For code that validates by hand and wants the same error:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { badUserInput } from '@nxgt/graphql-validation';
|
|
182
|
+
import { z } from 'zod';
|
|
183
|
+
|
|
184
|
+
const result = z.object({ code: z.string().length(4) }).safeParse({ code: 'abc' });
|
|
185
|
+
if (!result.success) throw badUserInput('Query.redeem', result.error);
|
|
186
|
+
// message: "Invalid arguments for Query.redeem. code: Too small: expected string to have exactly 4 characters"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import type { GraphQLError } from 'graphql';
|
|
191
|
+
import type { z } from 'zod';
|
|
192
|
+
|
|
193
|
+
declare function badUserInput(where: string, error: z.ZodError): GraphQLError;
|
|
194
|
+
```
|
package/docs/roadmap.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
What `@nxgt/graphql-validation` is heading for, phrased as what you get.
|
|
4
|
+
|
|
5
|
+
## Now
|
|
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`.
|
|
19
|
+
|
|
20
|
+
## Next
|
|
21
|
+
|
|
22
|
+
- **Your own formats**, named in `@constraint(format: "...")`.
|
|
23
|
+
- **Schemas that read the context**, `(args, context) => schema`, for checks
|
|
24
|
+
that need the request (uniqueness in a database, permissions).
|
|
25
|
+
|
|
26
|
+
## Shipped
|
|
27
|
+
|
|
28
|
+
- **A Zod codegen plugin**, [`@nxgt/graphql-codegen-zod`](https://www.npmjs.com/package/@nxgt/graphql-codegen-zod):
|
|
29
|
+
it writes the same schemas as source, from the same rules, so the types your
|
|
30
|
+
resolvers receive and the variables a client sends come from your SDL. This
|
|
31
|
+
package gains `@nxgt/graphql-validation/codegen` for it.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
One entry per error you can hit, headed by the message you will search for.
|
|
4
|
+
`withValidation` and `buildSchema` errors happen at startup; the last section
|
|
5
|
+
is the one a client sees.
|
|
6
|
+
|
|
7
|
+
- [Install](#install)
|
|
8
|
+
- [Startup](#startup)
|
|
9
|
+
- [Silent traps](#silent-traps)
|
|
10
|
+
- [Runtime](#runtime)
|
|
11
|
+
- [The bin](#the-bin)
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
### `TS2307: Cannot find module '@nxgt/graphql-validation'` (or its types)
|
|
16
|
+
|
|
17
|
+
**When:** type-checking an import of the package.
|
|
18
|
+
**Why:** the package is ESM with an `exports` map, which `moduleResolution`
|
|
19
|
+
`node`/`node10` ignores, and `nodenext` is not supported.
|
|
20
|
+
**Fix:**
|
|
21
|
+
|
|
22
|
+
```jsonc
|
|
23
|
+
{ "compilerOptions": { "moduleResolution": "bundler" } }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Startup
|
|
27
|
+
|
|
28
|
+
### `@constraint(minLength) on Query.check(age:) needs a String or an ID, not Int.`
|
|
29
|
+
|
|
30
|
+
**When:** calling `withValidation`.
|
|
31
|
+
**Why:** a rule narrows one kind of type. `format`, `minLength`, `maxLength`,
|
|
32
|
+
`startsWith`, `endsWith`, `contains`, `notContains` and `pattern` need a
|
|
33
|
+
`String` or an `ID`; `min`, `max`, `exclusiveMin`, `exclusiveMax` and
|
|
34
|
+
`multipleOf` need an `Int` or a `Float`. The same message, with `an Int or a
|
|
35
|
+
Float`, appears for a number rule on a `String`.
|
|
36
|
+
**Fix:** use a rule that fits the type.
|
|
37
|
+
|
|
38
|
+
```graphql
|
|
39
|
+
check(age: Int @constraint(min: 18)): Boolean
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `@constraint(minItems) on Query.check(name:) needs a list, not String.`
|
|
43
|
+
|
|
44
|
+
**When:** calling `withValidation`.
|
|
45
|
+
**Why:** `minItems` and `maxItems` apply to a list.
|
|
46
|
+
**Fix:** put them on a list argument, or use `minLength`/`maxLength` for a string.
|
|
47
|
+
|
|
48
|
+
```graphql
|
|
49
|
+
check(tags: [String!] @constraint(minItems: 1)): Boolean
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `@constraint(maxLength) on In.at needs a String or an ID, not DateTime.`
|
|
53
|
+
|
|
54
|
+
**When:** calling `withValidation`.
|
|
55
|
+
**Why:** a rule on a custom scalar, an enum or an input object (`..., not In.`)
|
|
56
|
+
has nothing to narrow: only `String`, `ID`, `Int`, `Float` and lists take
|
|
57
|
+
rules. The message names the place (`In.at`, `Query.check(input:)`) and the
|
|
58
|
+
type. This is checked in every input type, even one no argument reaches.
|
|
59
|
+
**Fix:** remove the directive; check a custom scalar in its own definition (for
|
|
60
|
+
example with `@nxgt/graphql-scalars`), or on a field with [`validated`](guide/errors.md#validated).
|
|
61
|
+
|
|
62
|
+
### `Unknown @constraint format "siret". Known formats: byte, date, date-time, email, ipv4, ipv6, uri, uuid.`
|
|
63
|
+
|
|
64
|
+
**When:** calling `withValidation`.
|
|
65
|
+
**Why:** `format` takes one of the listed names. Custom formats are on the
|
|
66
|
+
[roadmap](roadmap.md).
|
|
67
|
+
**Fix:** use a known format, or `pattern: "..."`.
|
|
68
|
+
|
|
69
|
+
### `Invalid @constraint pattern "[a-":`
|
|
70
|
+
|
|
71
|
+
**When:** calling `withValidation`.
|
|
72
|
+
**Why:** `pattern` is compiled with `new RegExp`; the text after the colon is
|
|
73
|
+
the runtime's own RegExp message, which differs between engines. V8 (Node)
|
|
74
|
+
says `Invalid regular expression: /[a-/: Unterminated character class`.
|
|
75
|
+
**Fix:** write a valid pattern. In SDL, escape backslashes: `"\\d+"`.
|
|
76
|
+
|
|
77
|
+
### `Unknown directive "@constraint".`
|
|
78
|
+
|
|
79
|
+
**When:** in the IDE (JetBrains GraphQL, VS Code GraphQL), on every `@constraint`
|
|
80
|
+
in a `.graphql` file. The server runs fine.
|
|
81
|
+
**Why:** the plugin reads your schema files and `constraintTypeDefs` is not one.
|
|
82
|
+
**Fix:** add the shipped file to `graphql.config.yml`, or write a copy into the
|
|
83
|
+
project and list that instead:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
schema:
|
|
87
|
+
- src/**/*.graphql
|
|
88
|
+
- node_modules/@nxgt/graphql-validation/graphql/constraint.graphqls
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
bunx nxgt-graphql-validation typedefs --out
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If the server scans the folder you write it to for its type definitions, the
|
|
96
|
+
copy declares the directive for the server too: drop `constraintTypeDefs`, or
|
|
97
|
+
keep the copy out of the scan. See the next entry.
|
|
98
|
+
|
|
99
|
+
### `There can be only one directive named "@constraint".`
|
|
100
|
+
|
|
101
|
+
**When:** building the schema.
|
|
102
|
+
**Why:** `@constraint` is declared twice, usually `constraintTypeDefs` plus a
|
|
103
|
+
`constraint.graphqls` copy that the server's type definitions scan picked up.
|
|
104
|
+
**Fix:** keep one. Either drop `constraintTypeDefs` and let the scanned copy
|
|
105
|
+
declare the directive, or keep `constraintTypeDefs` and exclude the copy from
|
|
106
|
+
the scan (it then serves the IDE only).
|
|
107
|
+
|
|
108
|
+
### `withValidation: this schema declares no @constraint directive. Add constraintTypeDefs to its type definitions.`
|
|
109
|
+
|
|
110
|
+
**When:** calling `withValidation`.
|
|
111
|
+
**Why:** the schema has no `@constraint` directive, so there is nothing to
|
|
112
|
+
read. Usually `constraintTypeDefs` was not added, or the schema is code-first and
|
|
113
|
+
declares no directive.
|
|
114
|
+
**Fix:**
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const typeDefs = [constraintTypeDefs, yourTypeDefs];
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `Directive "@constraint" may not be used on FIELD_DEFINITION.`
|
|
121
|
+
|
|
122
|
+
**When:** building the schema.
|
|
123
|
+
**Why:** `@constraint` is allowed on arguments and input fields only.
|
|
124
|
+
graphql-constraint-directive also allows output fields; this package refuses
|
|
125
|
+
them, since a resolver's result is the job of output scalars.
|
|
126
|
+
**Fix:** move it to the argument or input field, or use an output scalar.
|
|
127
|
+
|
|
128
|
+
### `Unknown argument "uniqueTypeName" on directive "@constraint".`
|
|
129
|
+
|
|
130
|
+
**When:** building the schema, migrating from graphql-constraint-directive.
|
|
131
|
+
**Why:** `uniqueTypeName` is a detail of that package's scalar wrapping and
|
|
132
|
+
does not exist here.
|
|
133
|
+
**Fix:** remove it.
|
|
134
|
+
|
|
135
|
+
### `@constraint on Named.name(style:) is not repeated on Person.name(style:), which resolves it (@constraint(maxLength: 5) there, no @constraint here). Write the same @constraint on both.`
|
|
136
|
+
|
|
137
|
+
**When:** calling `withValidation`, with a `@constraint` on an interface
|
|
138
|
+
field's argument.
|
|
139
|
+
**Why:** the objects resolve the field, not the interface, so a constraint
|
|
140
|
+
the object drops would go unchecked. `Named`, `Person` and `style` are your
|
|
141
|
+
interface, object and argument. When the object writes a different constraint
|
|
142
|
+
the message reads `... differs on Person.name(style:), which resolves it
|
|
143
|
+
(@constraint(maxLength: 5) there, @constraint(maxLength: 5, minLength: 1)
|
|
144
|
+
here). Write the same @constraint on both.`
|
|
145
|
+
**Fix:**
|
|
146
|
+
|
|
147
|
+
```graphql
|
|
148
|
+
interface Named { name(style: String @constraint(maxLength: 5)): String }
|
|
149
|
+
type Person implements Named {
|
|
150
|
+
name(style: String @constraint(maxLength: 5)): String
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `This schema's @constraint is not constraintTypeDefs': it is allowed on FIELD_DEFINITION; declares uniqueTypeName, which no rule reads. Declare it with constraintTypeDefs.`
|
|
155
|
+
|
|
156
|
+
**When:** calling `withValidation`, typically after migrating from
|
|
157
|
+
graphql-constraint-directive and keeping its SDL.
|
|
158
|
+
**Why:** the schema declares its own `@constraint`: extra locations would be
|
|
159
|
+
accepted and checked by nothing, extra arguments mean nothing here, and an
|
|
160
|
+
argument of another type would reach a rule as the wrong kind of value. The
|
|
161
|
+
message lists every difference: `repeatable, and only its first use is read`,
|
|
162
|
+
`allowed on <LOCATION>`, `declares <name>, which no rule reads`, or
|
|
163
|
+
`declares minLength: String, not Int`.
|
|
164
|
+
**Fix:** drop the old directive declaration and use the package's.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const typeDefs = [constraintTypeDefs, yourTypeDefs]; // no other `directive @constraint`
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `The default value of Query.a(name:) breaks its @constraint: Too small: expected string to have >=2 characters`
|
|
171
|
+
|
|
172
|
+
**When:** calling `withValidation`; for an argument (`Query.a(name:)`) or an
|
|
173
|
+
input field (`Page.size`).
|
|
174
|
+
**Why:** a request that leaves the value out would be refused for something the
|
|
175
|
+
client never sent. The text after the colon is the first Zod message.
|
|
176
|
+
The default is checked as graphql coerces it: a single value for a list
|
|
177
|
+
(`[String!] = "ab"` is `["ab"]`), an input object filled with its fields' own
|
|
178
|
+
defaults.
|
|
179
|
+
**Fix:** make the default satisfy the constraint, or relax the constraint.
|
|
180
|
+
|
|
181
|
+
```graphql
|
|
182
|
+
type Query { a(name: String = "xy" @constraint(minLength: 2)): Int }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `@constraint on @cached(ttl:) checks nothing: a directive's argument reaches no resolver. Remove it.`
|
|
186
|
+
|
|
187
|
+
**When:** calling `withValidation`, with `@constraint` written on an argument
|
|
188
|
+
of a directive definition (`directive @cached(ttl: Int @constraint(min: 1))`).
|
|
189
|
+
**Why:** graphql allows it there (it is an ARGUMENT_DEFINITION), but no
|
|
190
|
+
resolver receives a directive's argument, so the constraint would check
|
|
191
|
+
nothing.
|
|
192
|
+
**Fix:** remove it; check the directive's arguments where you read them.
|
|
193
|
+
|
|
194
|
+
## Silent traps
|
|
195
|
+
|
|
196
|
+
These raise no error.
|
|
197
|
+
|
|
198
|
+
### A constrained argument is never refused
|
|
199
|
+
|
|
200
|
+
**When:** an invalid value reaches the resolver.
|
|
201
|
+
**Why:** one of three causes. The schema declares `@constraint` but the constrained
|
|
202
|
+
types and fields have no SDL (code-first, or merged pieces): `@constraint` is
|
|
203
|
+
read from the SDL, and there is none.
|
|
204
|
+
Or `withValidation` was called before a resolver was attached: a resolver set on
|
|
205
|
+
a field afterwards replaces the check. Or the schema you serve is not the one
|
|
206
|
+
you passed (`withValidation` returns the same schema, so use either).
|
|
207
|
+
**Fix:** build from type definitions and call `withValidation` last.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const schema = makeExecutableSchema({ typeDefs, resolvers });
|
|
211
|
+
export default withValidation(schema); // last
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### `Invalid ISO datetime` for a date-time that looks right
|
|
215
|
+
|
|
216
|
+
**When:** `format: "date-time"` refuses `2024-03-10t12:00:00z`, `2024-03-10 12:00:00Z` or `2024-03-10T12:00Z`.
|
|
217
|
+
**Why:** only the canonical form is accepted: uppercase `T` and `Z`, seconds
|
|
218
|
+
present, then `Z` or a numeric offset.
|
|
219
|
+
**Fix:** send `2024-03-10T12:00:00Z` or `2024-03-10T12:00:00+02:00`.
|
|
220
|
+
|
|
221
|
+
## Runtime
|
|
222
|
+
|
|
223
|
+
### `Invalid arguments for Mutation.signUp. input.email: Invalid email address`
|
|
224
|
+
|
|
225
|
+
**When:** a request carries an argument that breaks a `@constraint` or a
|
|
226
|
+
`validated` schema.
|
|
227
|
+
**Why:** the message is `Invalid arguments for <Type>.<field>. <path>: <first
|
|
228
|
+
issue>`; `extensions.code` is `BAD_USER_INPUT` and `extensions.issues` lists
|
|
229
|
+
every issue. The resolver did not run.
|
|
230
|
+
**Fix:** send a valid value, or show each issue beside its field. See
|
|
231
|
+
[Errors](guide/errors.md). Common issues:
|
|
232
|
+
|
|
233
|
+
| Message | Cause |
|
|
234
|
+
| --- | --- |
|
|
235
|
+
| `Invalid email address` | `format: "email"` |
|
|
236
|
+
| `Invalid URL` | `format: "uri"`: not absolute, or a scheme other than `http`, `https`, `ftp` |
|
|
237
|
+
| `Invalid ISO datetime` / `Invalid ISO date` | `format: "date-time"` / `"date"` |
|
|
238
|
+
| `Too small: expected string to have >=2 characters` | `minLength: 2` |
|
|
239
|
+
| `Too big: expected array to have <=2 items` | `maxItems: 2` |
|
|
240
|
+
| `Too small: expected number to be >=18` | `min: 18` |
|
|
241
|
+
| `Invalid string: must match pattern /.../` | `pattern` |
|
|
242
|
+
|
|
243
|
+
## The bin
|
|
244
|
+
|
|
245
|
+
### `typedefs failed: <message>`
|
|
246
|
+
|
|
247
|
+
**When:** `nxgt-graphql-validation typedefs --out <file>` exits 1, for example
|
|
248
|
+
with `typedefs failed: EACCES: permission denied, mkdir 'schema'`, or
|
|
249
|
+
`EISDIR` when `<file>` is a folder.
|
|
250
|
+
**Why:** the file or its folder could not be written; the rest of the line is
|
|
251
|
+
the system's own message.
|
|
252
|
+
**Fix:** point `--out` at a file path you can write, or print to stdout and
|
|
253
|
+
redirect: `nxgt-graphql-validation typedefs > generated/graphql/constraint.graphqls`.
|
|
254
|
+
|
|
255
|
+
### `Unknown command "<x>".` or `Unexpected arguments: …`
|
|
256
|
+
|
|
257
|
+
**When:** the bin exits 2 and prints its usage.
|
|
258
|
+
**Why:** the only command is `typedefs`, and its only flag is `--out [<file>]` (and `--help`).
|
|
259
|
+
**Fix:** `nxgt-graphql-validation typedefs [--out [<file>]]`; `--help` prints
|
|
260
|
+
the usage and exits 0.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# The @constraint directive of @nxgt/graphql-validation, for your IDE.
|
|
2
|
+
# Generated by `nxgt-graphql-validation typedefs`: do not edit.
|
|
3
|
+
|
|
4
|
+
directive @constraint(
|
|
5
|
+
contains: String
|
|
6
|
+
endsWith: String
|
|
7
|
+
exclusiveMax: Float
|
|
8
|
+
exclusiveMin: Float
|
|
9
|
+
format: String
|
|
10
|
+
maxItems: Int
|
|
11
|
+
maxLength: Int
|
|
12
|
+
max: Float
|
|
13
|
+
minItems: Int
|
|
14
|
+
minLength: Int
|
|
15
|
+
min: Float
|
|
16
|
+
multipleOf: Float
|
|
17
|
+
notContains: String
|
|
18
|
+
pattern: String
|
|
19
|
+
startsWith: String
|
|
20
|
+
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
package/package.json
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@nxgt/graphql-validation",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Validate GraphQL arguments and input fields with @constraint directives, checked by Zod schemas, for graphql 16 and 17",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"nxgt-graphql-validation": "./dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"docs",
|
|
15
|
+
"graphql",
|
|
16
|
+
"README.md",
|
|
17
|
+
"package.json",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./codegen": {
|
|
27
|
+
"types": "./dist/codegen.d.ts",
|
|
28
|
+
"import": "./dist/codegen.js",
|
|
29
|
+
"default": "./dist/codegen.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"graphql",
|
|
35
|
+
"validation",
|
|
36
|
+
"constraint",
|
|
37
|
+
"directive",
|
|
38
|
+
"zod",
|
|
39
|
+
"bun",
|
|
40
|
+
"typescript"
|
|
41
|
+
],
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/softistx/nxgt-graphql.git",
|
|
45
|
+
"directory": "packages/graphql-validation"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"registry": "https://registry.npmjs.org",
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "bun run ../../build.ts",
|
|
53
|
+
"test": "bun test src",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"typedefs:write": "bun run src/cli.ts typedefs --out graphql/constraint.graphqls"
|
|
56
|
+
},
|
|
57
|
+
"nxgt": {
|
|
58
|
+
"entrypoints": [
|
|
59
|
+
"src/index.ts",
|
|
60
|
+
"src/codegen.ts",
|
|
61
|
+
"src/cli.ts"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/bun": "^1.4.0",
|
|
66
|
+
"graphql": "16.11.0",
|
|
67
|
+
"zod": "4.6.5"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"graphql": "^16.11.0 || ^17.0.0",
|
|
71
|
+
"typescript": "^6.0.3",
|
|
72
|
+
"zod": ">=4.6.5 <5"
|
|
73
|
+
}
|
|
74
|
+
}
|