@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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -2
  3. package/dist/bad-user-input.d.ts +35 -0
  4. package/dist/bad-user-input.d.ts.map +1 -0
  5. package/dist/builder/args-schema.d.ts +17 -0
  6. package/dist/builder/args-schema.d.ts.map +1 -0
  7. package/dist/builder/check-constraints.d.ts +15 -0
  8. package/dist/builder/check-constraints.d.ts.map +1 -0
  9. package/dist/builder/constraints.d.ts +16 -0
  10. package/dist/builder/constraints.d.ts.map +1 -0
  11. package/dist/builder/input-code.d.ts +29 -0
  12. package/dist/builder/input-code.d.ts.map +1 -0
  13. package/dist/builder/input-schema.d.ts +31 -0
  14. package/dist/builder/input-schema.d.ts.map +1 -0
  15. package/dist/builder/leaf.d.ts +24 -0
  16. package/dist/builder/leaf.d.ts.map +1 -0
  17. package/dist/chunks/index-rbxe5m3y.js +319 -0
  18. package/dist/chunks/index-rbxe5m3y.js.map +37 -0
  19. package/dist/chunks/index-s1kkmg35.js +266 -0
  20. package/dist/chunks/index-s1kkmg35.js.map +14 -0
  21. package/dist/cli.d.ts +3 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +69 -0
  24. package/dist/cli.js.map +11 -0
  25. package/dist/codegen.d.ts +4 -0
  26. package/dist/codegen.d.ts.map +1 -0
  27. package/dist/codegen.js +56 -0
  28. package/dist/codegen.js.map +10 -0
  29. package/dist/constraint-directive.d.ts +21 -0
  30. package/dist/constraint-directive.d.ts.map +1 -0
  31. package/dist/formats/all.d.ts +9 -0
  32. package/dist/formats/all.d.ts.map +1 -0
  33. package/dist/formats/byte.d.ts +2 -0
  34. package/dist/formats/byte.d.ts.map +1 -0
  35. package/dist/formats/date-time.d.ts +2 -0
  36. package/dist/formats/date-time.d.ts.map +1 -0
  37. package/dist/formats/date.d.ts +2 -0
  38. package/dist/formats/date.d.ts.map +1 -0
  39. package/dist/formats/email.d.ts +2 -0
  40. package/dist/formats/email.d.ts.map +1 -0
  41. package/dist/formats/format.d.ts +15 -0
  42. package/dist/formats/format.d.ts.map +1 -0
  43. package/dist/formats/index.d.ts +13 -0
  44. package/dist/formats/index.d.ts.map +1 -0
  45. package/dist/formats/ipv4.d.ts +2 -0
  46. package/dist/formats/ipv4.d.ts.map +1 -0
  47. package/dist/formats/ipv6.d.ts +2 -0
  48. package/dist/formats/ipv6.d.ts.map +1 -0
  49. package/dist/formats/uri.d.ts +2 -0
  50. package/dist/formats/uri.d.ts.map +1 -0
  51. package/dist/formats/uuid.d.ts +2 -0
  52. package/dist/formats/uuid.d.ts.map +1 -0
  53. package/dist/index.d.ts +6 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +84 -0
  56. package/dist/index.js.map +12 -0
  57. package/dist/rules/all.d.ts +16 -0
  58. package/dist/rules/all.d.ts.map +1 -0
  59. package/dist/rules/contains.d.ts +2 -0
  60. package/dist/rules/contains.d.ts.map +1 -0
  61. package/dist/rules/ends-with.d.ts +2 -0
  62. package/dist/rules/ends-with.d.ts.map +1 -0
  63. package/dist/rules/exclusive-max.d.ts +2 -0
  64. package/dist/rules/exclusive-max.d.ts.map +1 -0
  65. package/dist/rules/exclusive-min.d.ts +2 -0
  66. package/dist/rules/exclusive-min.d.ts.map +1 -0
  67. package/dist/rules/format.d.ts +2 -0
  68. package/dist/rules/format.d.ts.map +1 -0
  69. package/dist/rules/index.d.ts +28 -0
  70. package/dist/rules/index.d.ts.map +1 -0
  71. package/dist/rules/max-items.d.ts +2 -0
  72. package/dist/rules/max-items.d.ts.map +1 -0
  73. package/dist/rules/max-length.d.ts +2 -0
  74. package/dist/rules/max-length.d.ts.map +1 -0
  75. package/dist/rules/max.d.ts +2 -0
  76. package/dist/rules/max.d.ts.map +1 -0
  77. package/dist/rules/min-items.d.ts +2 -0
  78. package/dist/rules/min-items.d.ts.map +1 -0
  79. package/dist/rules/min-length.d.ts +2 -0
  80. package/dist/rules/min-length.d.ts.map +1 -0
  81. package/dist/rules/min.d.ts +2 -0
  82. package/dist/rules/min.d.ts.map +1 -0
  83. package/dist/rules/multiple-of.d.ts +2 -0
  84. package/dist/rules/multiple-of.d.ts.map +1 -0
  85. package/dist/rules/not-contains.d.ts +2 -0
  86. package/dist/rules/not-contains.d.ts.map +1 -0
  87. package/dist/rules/pattern.d.ts +2 -0
  88. package/dist/rules/pattern.d.ts.map +1 -0
  89. package/dist/rules/rule.d.ts +55 -0
  90. package/dist/rules/rule.d.ts.map +1 -0
  91. package/dist/rules/starts-with.d.ts +2 -0
  92. package/dist/rules/starts-with.d.ts.map +1 -0
  93. package/dist/typedefs-command.d.ts +10 -0
  94. package/dist/typedefs-command.d.ts.map +1 -0
  95. package/dist/validated.d.ts +22 -0
  96. package/dist/validated.d.ts.map +1 -0
  97. package/dist/with-validation.d.ts +26 -0
  98. package/dist/with-validation.d.ts.map +1 -0
  99. package/docs/README.md +15 -0
  100. package/docs/guide/constraints.md +291 -0
  101. package/docs/guide/errors.md +194 -0
  102. package/docs/roadmap.md +31 -0
  103. package/docs/troubleshooting.md +260 -0
  104. package/graphql/constraint.graphqls +20 -0
  105. 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
+ ```
@@ -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
- "name": "@nxgt/graphql-validation",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }