@nxgt/graphql-validation 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -2
- package/dist/field-issue-of.d.ts +28 -0
- package/dist/field-issue-of.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -1
- package/dist/index.js.map +5 -3
- package/dist/invalid-input.d.ts +61 -0
- package/dist/invalid-input.d.ts.map +1 -0
- package/docs/README.md +1 -0
- package/docs/guide/constraints.md +5 -0
- package/docs/guide/errors.md +3 -1
- package/docs/guide/invalid-input.md +240 -0
- package/docs/roadmap.md +21 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -188,6 +188,49 @@ const resolvers = {
|
|
|
188
188
|
};
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
### Answering invalid input as one error
|
|
192
|
+
|
|
193
|
+
For an API that answers invalid input as one 400 error with a list of fields,
|
|
194
|
+
`invalidInputError` builds that error from your own codes, translated by your
|
|
195
|
+
i18n function (each message is the code itself without one). `fieldsError`
|
|
196
|
+
does the same for a refusal of your own (a 409 conflict) that names the
|
|
197
|
+
fields at fault. Neither changes what `withValidation` raises.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { inputPath, invalidInputError, type Translate } from '@nxgt/graphql-validation';
|
|
201
|
+
|
|
202
|
+
// Your catalogue: code and params in, the message in the request's language out.
|
|
203
|
+
const t: Translate = (code, params) => i18n.t(code, params);
|
|
204
|
+
|
|
205
|
+
throw invalidInputError(
|
|
206
|
+
[
|
|
207
|
+
{ path: inputPath(['input', 'person', 'firstName']), code: 'validation.errors.too-short', params: { min: 2 } },
|
|
208
|
+
{ path: 'person.lastName', code: 'validation.errors.required' },
|
|
209
|
+
],
|
|
210
|
+
{ translate: t },
|
|
211
|
+
);
|
|
212
|
+
// extensions: { code: 'validation.errors.invalid-input', http: { status: 400 },
|
|
213
|
+
// fields: [{ path: 'person.firstName', code: 'validation.errors.too-short',
|
|
214
|
+
// message: 'At least 2 characters', params: { min: 2 } }, ...] }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Fields keep the order given; `params` is written only when it has a key
|
|
218
|
+
(never `{}`); `GraphQLError`'s options (`originalError`, `path`, …) are kept,
|
|
219
|
+
but an `extensions` among them is replaced. From a Zod parse,
|
|
220
|
+
`fieldIssueOf` maps each issue to a field and a code (`too-short`,
|
|
221
|
+
`invalid-format`, …):
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { fieldIssueOf, invalidInputError } from '@nxgt/graphql-validation';
|
|
225
|
+
|
|
226
|
+
const result = PersonInput.safeParse(input);
|
|
227
|
+
if (!result.success)
|
|
228
|
+
throw invalidInputError(result.error.issues.map(fieldIssueOf), { translate: t });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The codes, and `fieldsError`, are in
|
|
232
|
+
[Answering invalid input as one error](docs/guide/invalid-input.md).
|
|
233
|
+
|
|
191
234
|
## IDE support
|
|
192
235
|
|
|
193
236
|
An IDE's GraphQL plugin (JetBrains GraphQL, VS Code GraphQL) reads your `.graphql`
|
|
@@ -231,6 +274,13 @@ the directive declared is this package's. Both at once throw
|
|
|
231
274
|
| `withValidation(schema, options?)` | checks every `@constraint` of a schema before its resolvers run; `options.formats` adds your own formats |
|
|
232
275
|
| `validated(schema \| shape, resolver)` | a resolver whose arguments a Zod schema checks |
|
|
233
276
|
| `badUserInput(where, zodError)` | builds the error above, for code that validates by hand |
|
|
277
|
+
| `invalidInputError(issues, options?)` | one 400 error, `extensions: { code: 'validation.errors.invalid-input', http, fields }`; `options`: `translate` and `GraphQLError`'s own |
|
|
278
|
+
| `fieldsError(code, issues, options?)` | a refusal of your own naming fields, `extensions: { code, params?, http?, fields }`; `options` adds `status` and `params` |
|
|
279
|
+
| `inputPath(path)` | `['input', 'person', 'firstName']` as `person.firstName`: dotted, the leading `input` argument dropped; a symbol written as `String(key)` |
|
|
280
|
+
| `fieldIssueOf(issue)` | a Zod issue as a `FieldIssue`: `inputPath(issue.path)` and a code under `validation.errors.` (`too-short`, `too-long`, `out-of-range`, `too-few`, `too-many`, `invalid-format`, `invalid`), no params |
|
|
281
|
+
| `FIELD_CODES` | those codes, keyed `tooShort`, `tooLong`, `outOfRange`, `tooFew`, `tooMany`, `invalidFormat`, `invalid` |
|
|
282
|
+
| `INVALID_INPUT` | `'validation.errors.invalid-input'` |
|
|
283
|
+
| `FieldIssue`, `FieldError`, `IssueParams`, `Translate`, `InvalidInputOptions`, `FieldsErrorOptions` | their types: an issue you write, a `fields` entry, its params, your translator, the options |
|
|
234
284
|
| `ValidationIssue`, `BadUserInputExtensions` | the types of `extensions.issues` and `extensions` |
|
|
235
285
|
| `ConstraintArgument` | the type of `issues[].constraint`: `'minLength' \| 'format' \| ...` |
|
|
236
286
|
| `ArgsSchema`, `SchemaOf` | the types `validated` accepts |
|
|
@@ -290,6 +340,6 @@ Every startup error and its fix is in [Troubleshooting](docs/troubleshooting.md)
|
|
|
290
340
|
## Documentation
|
|
291
341
|
|
|
292
342
|
- [Documentation index](docs/README.md)
|
|
293
|
-
- Guides: [Constraints](docs/guide/constraints.md), [Errors](docs/guide/errors.md)
|
|
343
|
+
- Guides: [Constraints](docs/guide/constraints.md), [Errors](docs/guide/errors.md), [Answering invalid input as one error](docs/guide/invalid-input.md)
|
|
294
344
|
- [Troubleshooting](docs/troubleshooting.md)
|
|
295
|
-
- [Roadmap](docs/roadmap.md):
|
|
345
|
+
- [Roadmap](docs/roadmap.md): schemas that read the context, emitting the invalid-input shape directly
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { type FieldIssue } from './invalid-input.js';
|
|
3
|
+
/**
|
|
4
|
+
* The codes `fieldIssueOf` gives, each under `validation.errors.`: a string's
|
|
5
|
+
* length (`too-short`, `too-long`), a number's or bigint's bound or multiple
|
|
6
|
+
* (`out-of-range`), a list's or set's size (`too-few`, `too-many`), a named
|
|
7
|
+
* format or a pattern (`invalid-format`), anything else (`invalid`).
|
|
8
|
+
*/
|
|
9
|
+
export declare const FIELD_CODES: {
|
|
10
|
+
readonly tooShort: "validation.errors.too-short";
|
|
11
|
+
readonly tooLong: "validation.errors.too-long";
|
|
12
|
+
readonly outOfRange: "validation.errors.out-of-range";
|
|
13
|
+
readonly tooFew: "validation.errors.too-few";
|
|
14
|
+
readonly tooMany: "validation.errors.too-many";
|
|
15
|
+
readonly invalidFormat: "validation.errors.invalid-format";
|
|
16
|
+
readonly invalid: "validation.errors.invalid";
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A Zod issue as a `FieldIssue`, for `invalidInputError`: its path through
|
|
20
|
+
* `inputPath` (dotted, a leading `input` dropped) and one of `FIELD_CODES`,
|
|
21
|
+
* without params. `too-short` / `too-long` for a string's length,
|
|
22
|
+
* `out-of-range` for a number's or bigint's bound or a multiple, `too-few` /
|
|
23
|
+
* `too-many` for a list's or set's size, `invalid-format` for a named format
|
|
24
|
+
* or a pattern, `invalid` for anything else (a prefix, a suffix or a
|
|
25
|
+
* substring included).
|
|
26
|
+
*/
|
|
27
|
+
export declare function fieldIssueOf(issue: z.core.$ZodIssue): FieldIssue;
|
|
28
|
+
//# sourceMappingURL=field-issue-of.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-issue-of.d.ts","sourceRoot":"","sources":["../src/field-issue-of.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAE,KAAK,UAAU,EAAa,MAAM,iBAAiB,CAAC;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,WAAW;;;;;;;;CAQd,CAAC;AASX;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,UAAU,CAEhE"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { type BadUserInputExtensions, badUserInput, type ValidationIssue, } from './bad-user-input.js';
|
|
2
2
|
export { constraintTypeDefs } from './constraint-directive.js';
|
|
3
|
+
export { FIELD_CODES, fieldIssueOf } from './field-issue-of.js';
|
|
3
4
|
export type { FormatName } from './formats/index.js';
|
|
4
5
|
export type { StringSchema } from './formats/format.js';
|
|
5
6
|
export type { FormatSchemas, OwnFormats } from './formats/registry.js';
|
|
7
|
+
export { type FieldError, type FieldIssue, type FieldsErrorOptions, fieldsError, INVALID_INPUT, type InvalidInputOptions, type IssueParams, inputPath, invalidInputError, type Translate, } from './invalid-input.js';
|
|
6
8
|
export type { ConstraintArgument } from './rules/index.js';
|
|
7
9
|
export { type ArgsSchema, type SchemaOf, validated } from './validated.js';
|
|
8
10
|
export { type WithValidationOptions, withValidation, } from './with-validation.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,sBAAsB,EAC3B,YAAY,EACZ,KAAK,eAAe,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACpE,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EACN,KAAK,qBAAqB,EAC1B,cAAc,GACd,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,sBAAsB,EAC3B,YAAY,EACZ,KAAK,eAAe,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EACN,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,WAAW,EACX,aAAa,EACb,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,SAAS,EACT,iBAAiB,EACjB,KAAK,SAAS,GACd,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EACN,KAAK,qBAAqB,EAC1B,cAAc,GACd,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,99 @@ async function parseArgs(schema, args, where, constraintOf) {
|
|
|
32
32
|
throw errorOf(where, issuesOf(result.error, constraintOf));
|
|
33
33
|
return result.data;
|
|
34
34
|
}
|
|
35
|
+
// src/invalid-input.ts
|
|
36
|
+
import { GraphQLError as GraphQLError2 } from "graphql";
|
|
37
|
+
var INVALID_INPUT = "validation.errors.invalid-input";
|
|
38
|
+
function paramsOf(params) {
|
|
39
|
+
return params && Object.keys(params).length > 0 ? params : undefined;
|
|
40
|
+
}
|
|
41
|
+
function messageOf(code, params, translate) {
|
|
42
|
+
return translate?.(code, params) ?? code;
|
|
43
|
+
}
|
|
44
|
+
function fieldsOf(issues, translate) {
|
|
45
|
+
return issues.map(({ path, code, params: given }) => {
|
|
46
|
+
const params = paramsOf(given);
|
|
47
|
+
return {
|
|
48
|
+
path,
|
|
49
|
+
code,
|
|
50
|
+
message: messageOf(code, params, translate),
|
|
51
|
+
...params && { params }
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function invalidInputError(issues, options = {}) {
|
|
56
|
+
const { translate, ...graphql } = options;
|
|
57
|
+
return new GraphQLError2(messageOf(INVALID_INPUT, undefined, translate), {
|
|
58
|
+
...graphql,
|
|
59
|
+
extensions: {
|
|
60
|
+
code: INVALID_INPUT,
|
|
61
|
+
http: { status: 400 },
|
|
62
|
+
fields: fieldsOf(issues, translate)
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function fieldsError(code, issues, options = {}) {
|
|
67
|
+
const { translate, status, params: given, ...graphql } = options;
|
|
68
|
+
const params = paramsOf(given);
|
|
69
|
+
return new GraphQLError2(messageOf(code, params, translate), {
|
|
70
|
+
...graphql,
|
|
71
|
+
extensions: {
|
|
72
|
+
code,
|
|
73
|
+
...params && { params },
|
|
74
|
+
...status && { http: { status } },
|
|
75
|
+
fields: fieldsOf(issues, translate)
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function inputPath(path) {
|
|
80
|
+
const [first, ...rest] = path;
|
|
81
|
+
return (first === "input" && rest.length > 0 ? rest : path).map((key) => String(key)).join(".");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/field-issue-of.ts
|
|
85
|
+
var FIELD_CODES = {
|
|
86
|
+
tooShort: "validation.errors.too-short",
|
|
87
|
+
tooLong: "validation.errors.too-long",
|
|
88
|
+
outOfRange: "validation.errors.out-of-range",
|
|
89
|
+
tooFew: "validation.errors.too-few",
|
|
90
|
+
tooMany: "validation.errors.too-many",
|
|
91
|
+
invalidFormat: "validation.errors.invalid-format",
|
|
92
|
+
invalid: "validation.errors.invalid"
|
|
93
|
+
};
|
|
94
|
+
var TEXT_CHECKS = new Set([
|
|
95
|
+
"starts_with",
|
|
96
|
+
"ends_with",
|
|
97
|
+
"includes"
|
|
98
|
+
]);
|
|
99
|
+
function fieldIssueOf(issue) {
|
|
100
|
+
return { path: inputPath(issue.path), code: codeOf(issue) };
|
|
101
|
+
}
|
|
102
|
+
function codeOf(issue) {
|
|
103
|
+
switch (issue.code) {
|
|
104
|
+
case "too_small":
|
|
105
|
+
case "too_big": {
|
|
106
|
+
const small = issue.code === "too_small";
|
|
107
|
+
switch (issue.origin) {
|
|
108
|
+
case "string":
|
|
109
|
+
return small ? FIELD_CODES.tooShort : FIELD_CODES.tooLong;
|
|
110
|
+
case "number":
|
|
111
|
+
case "bigint":
|
|
112
|
+
return FIELD_CODES.outOfRange;
|
|
113
|
+
case "array":
|
|
114
|
+
case "set":
|
|
115
|
+
return small ? FIELD_CODES.tooFew : FIELD_CODES.tooMany;
|
|
116
|
+
default:
|
|
117
|
+
return FIELD_CODES.invalid;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
case "not_multiple_of":
|
|
121
|
+
return FIELD_CODES.outOfRange;
|
|
122
|
+
case "invalid_format":
|
|
123
|
+
return TEXT_CHECKS.has(issue.format) ? FIELD_CODES.invalid : FIELD_CODES.invalidFormat;
|
|
124
|
+
default:
|
|
125
|
+
return FIELD_CODES.invalid;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
35
128
|
// src/validated.ts
|
|
36
129
|
import { z } from "zod";
|
|
37
130
|
function validated(schema, resolve) {
|
|
@@ -87,11 +180,17 @@ function sameFormats(a, b) {
|
|
|
87
180
|
return names.length === Object.keys(b).length && names.every((name) => Object.hasOwn(b, name) && a[name] === b[name]);
|
|
88
181
|
}
|
|
89
182
|
export {
|
|
183
|
+
FIELD_CODES,
|
|
184
|
+
INVALID_INPUT,
|
|
90
185
|
badUserInput,
|
|
91
186
|
constraintTypeDefs2 as constraintTypeDefs,
|
|
187
|
+
fieldIssueOf,
|
|
188
|
+
fieldsError,
|
|
189
|
+
inputPath,
|
|
190
|
+
invalidInputError,
|
|
92
191
|
validated,
|
|
93
192
|
withValidation
|
|
94
193
|
};
|
|
95
194
|
|
|
96
|
-
//# debugId=
|
|
195
|
+
//# debugId=BC885DEBB67481A464756E2164756E21
|
|
97
196
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/bad-user-input.ts", "../src/validated.ts", "../src/with-validation.ts"],
|
|
3
|
+
"sources": ["../src/bad-user-input.ts", "../src/invalid-input.ts", "../src/field-issue-of.ts", "../src/validated.ts", "../src/with-validation.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { GraphQLError } from 'graphql';\nimport type { z } from 'zod';\nimport type { ConstraintArgument } from './rules';\n\n/** One reason an input was refused, at its path inside the arguments. */\nexport interface ValidationIssue {\n\t/** `['input', 'address', 'zip']`: the argument, then the input fields. */\n\treadonly path: readonly (string | number)[];\n\treadonly message: string;\n\t/** Zod's issue code: `too_small`, `invalid_format`, `custom`, ... */\n\treadonly code: string;\n\t/**\n\t * The `@constraint` argument that refused (`minLength`, `format`), when a\n\t * directive did: stable across Zod versions, unlike `code`. Absent for a\n\t * refusal of `validated()`'s own schema.\n\t */\n\treadonly constraint?: ConstraintArgument;\n}\n\n/** The `extensions` of the error an invalid input becomes. */\nexport type BadUserInputExtensions = {\n\treadonly code: 'BAD_USER_INPUT';\n\treadonly issues: readonly ValidationIssue[];\n};\n\n/** Names the `@constraint` argument behind an issue, when there is one. */\nexport type ConstraintOf = (\n\tissue: z.core.$ZodIssue,\n) => ConstraintArgument | undefined;\n\nfunction issuesOf(\n\terror: z.ZodError,\n\tconstraintOf?: ConstraintOf,\n): ValidationIssue[] {\n\treturn error.issues.map((issue) => {\n\t\tconst path = issue.path.map((key) =>\n\t\t\ttypeof key === 'number' ? key : String(key),\n\t\t);\n\t\tconst constraint = constraintOf?.(issue);\n\t\treturn constraint\n\t\t\t? { path, message: issue.message, code: issue.code, constraint }\n\t\t\t: { path, message: issue.message, code: issue.code };\n\t});\n}\n\nfunction errorOf(where: string, issues: ValidationIssue[]): GraphQLError {\n\tconst [first] = issues;\n\tconst reason = first\n\t\t? `${first.path.join('.') || 'arguments'}: ${first.message}`\n\t\t: 'refused';\n\tconst extensions: BadUserInputExtensions = { code: 'BAD_USER_INPUT', issues };\n\treturn new GraphQLError(`Invalid arguments for ${where}. ${reason}`, {\n\t\textensions,\n\t});\n}\n\n/**\n * The one error an invalid input becomes: `extensions.code` is\n * `BAD_USER_INPUT`, the code Apollo and most clients already read, and\n * `extensions.issues` lists every reason with its path, so a form can show\n * each one beside its field. The message names the field and the first\n * reason, for a log line.\n */\nexport function badUserInput(where: string, error: z.ZodError): GraphQLError {\n\treturn errorOf(where, issuesOf(error));\n}\n\n/** The parsed arguments, or the error above, its issues named by `constraintOf`. */\nexport async function parseArgs(\n\tschema: z.ZodType,\n\targs: unknown,\n\twhere: string,\n\tconstraintOf?: ConstraintOf,\n): Promise<unknown> {\n\tconst result = await schema.safeParseAsync(args);\n\tif (!result.success)\n\t\tthrow errorOf(where, issuesOf(result.error, constraintOf));\n\treturn result.data;\n}\n",
|
|
6
|
+
"import { GraphQLError, type GraphQLErrorOptions } from 'graphql';\n\n/** The code of the error `invalidInputError` builds, and its default message. */\nexport const INVALID_INPUT = 'validation.errors.invalid-input';\n\n/** Values a message is built from, such as `{ min: 2 }`. */\nexport type IssueParams = Record<string, string | number>;\n\n/** One field at fault: where it is, why, and what the reason's message needs. */\nexport interface FieldIssue {\n\t/** The field inside the input, dotted: `person.firstName`, `tags.1`. */\n\treadonly path: string;\n\t/** A code your client and your translations know: `validation.errors.too-short`. */\n\treadonly code: string;\n\t/** Values the message needs; an object with no key counts as none. */\n\treadonly params?: IssueParams;\n}\n\n/** One entry of `extensions.fields`: a `FieldIssue` with its message. */\nexport interface FieldError {\n\treadonly path: string;\n\treadonly code: string;\n\t/** `translate(code, params)`, or the code itself without a translator. */\n\treadonly message: string;\n\t/** Present only when the issue had params with at least one key. */\n\treadonly params?: IssueParams;\n}\n\n/** Turns a code (and its params) into the text a person reads. */\nexport type Translate = (code: string, params?: IssueParams) => string;\n\n/** The options of `invalidInputError`: GraphQL's own, and a translator. */\nexport type InvalidInputOptions = GraphQLErrorOptions & {\n\treadonly translate?: Translate;\n};\n\n/** The options of `fieldsError`: also an HTTP status and the error's own params. */\nexport type FieldsErrorOptions = InvalidInputOptions & {\n\t/** `extensions.http.status`, for a server that reads it (409, ...). */\n\treadonly status?: number;\n\treadonly params?: IssueParams;\n};\n\n/** `params` itself, or `undefined` when it holds no key: never `{}`. */\nfunction paramsOf(params: IssueParams | undefined): IssueParams | undefined {\n\treturn params && Object.keys(params).length > 0 ? params : undefined;\n}\n\nfunction messageOf(\n\tcode: string,\n\tparams: IssueParams | undefined,\n\ttranslate: Translate | undefined,\n): string {\n\treturn translate?.(code, params) ?? code;\n}\n\nfunction fieldsOf(\n\tissues: readonly FieldIssue[],\n\ttranslate: Translate | undefined,\n): FieldError[] {\n\treturn issues.map(({ path, code, params: given }) => {\n\t\tconst params = paramsOf(given);\n\t\treturn {\n\t\t\tpath,\n\t\t\tcode,\n\t\t\tmessage: messageOf(code, params, translate),\n\t\t\t...(params && { params }),\n\t\t};\n\t});\n}\n\n/**\n * One 400 error listing every field at fault, for an API that answers invalid\n * input this way: `extensions` is `{ code: 'validation.errors.invalid-input',\n * http: { status: 400 }, fields }`, each field with its translated message\n * (the code itself without `translate`), in the order given. GraphQL's\n * options (`originalError`, `path`, `nodes`, ...) are kept; an `extensions`\n * among them is replaced by this one.\n */\nexport function invalidInputError(\n\tissues: readonly FieldIssue[],\n\toptions: InvalidInputOptions = {},\n): GraphQLError {\n\tconst { translate, ...graphql } = options;\n\treturn new GraphQLError(messageOf(INVALID_INPUT, undefined, translate), {\n\t\t...graphql,\n\t\textensions: {\n\t\t\tcode: INVALID_INPUT,\n\t\t\thttp: { status: 400 },\n\t\t\tfields: fieldsOf(issues, translate),\n\t\t},\n\t});\n}\n\n/**\n * A refusal of your own (a 409 conflict, say) that names the fields at\n * fault: `extensions` is `{ code, params?, http?: { status }, fields }`, the\n * fields worded as `invalidInputError` words them, the message\n * `translate(code, params)` or the code. An `extensions` among the options\n * is replaced by this one.\n */\nexport function fieldsError(\n\tcode: string,\n\tissues: readonly FieldIssue[],\n\toptions: FieldsErrorOptions = {},\n): GraphQLError {\n\tconst { translate, status, params: given, ...graphql } = options;\n\tconst params = paramsOf(given);\n\treturn new GraphQLError(messageOf(code, params, translate), {\n\t\t...graphql,\n\t\textensions: {\n\t\t\tcode,\n\t\t\t...(params && { params }),\n\t\t\t...(status && { http: { status } }),\n\t\t\tfields: fieldsOf(issues, translate),\n\t\t},\n\t});\n}\n\n/**\n * A GraphQL or Zod path as a `FieldIssue` path: dotted, without the leading\n * `input` argument when a field follows it, a symbol written as\n * `String(key)`. `['input', 'person', 'firstName']` is `person.firstName`,\n * `['input', 'tags', 1]` is `tags.1`, `['first']` and `['input']` stay as\n * they are.\n */\nexport function inputPath(path: readonly PropertyKey[]): string {\n\tconst [first, ...rest] = path;\n\treturn (first === 'input' && rest.length > 0 ? rest : path)\n\t\t.map((key) => String(key))\n\t\t.join('.');\n}\n",
|
|
7
|
+
"import type { z } from 'zod';\nimport { type FieldIssue, inputPath } from './invalid-input';\n\n/**\n * The codes `fieldIssueOf` gives, each under `validation.errors.`: a string's\n * length (`too-short`, `too-long`), a number's or bigint's bound or multiple\n * (`out-of-range`), a list's or set's size (`too-few`, `too-many`), a named\n * format or a pattern (`invalid-format`), anything else (`invalid`).\n */\nexport const FIELD_CODES = {\n\ttooShort: 'validation.errors.too-short',\n\ttooLong: 'validation.errors.too-long',\n\toutOfRange: 'validation.errors.out-of-range',\n\ttooFew: 'validation.errors.too-few',\n\ttooMany: 'validation.errors.too-many',\n\tinvalidFormat: 'validation.errors.invalid-format',\n\tinvalid: 'validation.errors.invalid',\n} as const;\n\n/** The string checks Zod reports as `invalid_format` that are not a format. */\nconst TEXT_CHECKS: ReadonlySet<string> = new Set([\n\t'starts_with',\n\t'ends_with',\n\t'includes',\n]);\n\n/**\n * A Zod issue as a `FieldIssue`, for `invalidInputError`: its path through\n * `inputPath` (dotted, a leading `input` dropped) and one of `FIELD_CODES`,\n * without params. `too-short` / `too-long` for a string's length,\n * `out-of-range` for a number's or bigint's bound or a multiple, `too-few` /\n * `too-many` for a list's or set's size, `invalid-format` for a named format\n * or a pattern, `invalid` for anything else (a prefix, a suffix or a\n * substring included).\n */\nexport function fieldIssueOf(issue: z.core.$ZodIssue): FieldIssue {\n\treturn { path: inputPath(issue.path), code: codeOf(issue) };\n}\n\nfunction codeOf(issue: z.core.$ZodIssue): string {\n\tswitch (issue.code) {\n\t\tcase 'too_small':\n\t\tcase 'too_big': {\n\t\t\tconst small = issue.code === 'too_small';\n\t\t\tswitch (issue.origin) {\n\t\t\t\tcase 'string':\n\t\t\t\t\treturn small ? FIELD_CODES.tooShort : FIELD_CODES.tooLong;\n\t\t\t\tcase 'number':\n\t\t\t\tcase 'bigint':\n\t\t\t\t\treturn FIELD_CODES.outOfRange;\n\t\t\t\tcase 'array':\n\t\t\t\tcase 'set':\n\t\t\t\t\treturn small ? FIELD_CODES.tooFew : FIELD_CODES.tooMany;\n\t\t\t\tdefault:\n\t\t\t\t\treturn FIELD_CODES.invalid;\n\t\t\t}\n\t\t}\n\t\tcase 'not_multiple_of':\n\t\t\treturn FIELD_CODES.outOfRange;\n\t\tcase 'invalid_format':\n\t\t\t// A prefix, a suffix or a substring is not a format: only a named\n\t\t\t// format (`email`, `uuid`, …) or a pattern is.\n\t\t\treturn TEXT_CHECKS.has(issue.format)\n\t\t\t\t? FIELD_CODES.invalid\n\t\t\t\t: FIELD_CODES.invalidFormat;\n\t\tdefault:\n\t\t\treturn FIELD_CODES.invalid;\n\t}\n}\n",
|
|
6
8
|
"import type { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql';\nimport { z } from 'zod';\nimport { parseArgs } from './bad-user-input';\n\n/** An object schema of the arguments, or its shape. */\nexport type ArgsSchema = z.ZodType<Record<string, unknown>> | z.ZodRawShape;\n\n/** The schema `ArgsSchema` stands for. */\nexport type SchemaOf<S extends ArgsSchema> = S extends z.ZodType\n\t? S\n\t: z.ZodObject<S & z.ZodRawShape>;\n\n/**\n * A resolver whose arguments are checked by `schema` before it runs, for what\n * a `@constraint` cannot say: a rule across arguments, a business refinement,\n * an async check. It receives the parsed arguments (`z.output`), defaults and\n * transforms applied; an invalid input is the same `BAD_USER_INPUT` error as\n * the directives'.\n *\n * ```ts\n * signUp: validated(\n * z.object({ input: signUpSchema }).refine(({ input }) => input.password !== input.email),\n * (_, { input }) => users.create(input),\n * )\n * ```\n */\nexport function validated<\n\tS extends ArgsSchema,\n\tTSource = unknown,\n\tTContext = unknown,\n\tTResult = unknown,\n>(\n\tschema: S,\n\tresolve: (\n\t\tsource: TSource,\n\t\targs: z.output<SchemaOf<S>>,\n\t\tcontext: TContext,\n\t\tinfo: GraphQLResolveInfo,\n\t) => TResult,\n): GraphQLFieldResolver<\n\tTSource,\n\tTContext,\n\tz.input<SchemaOf<S>>,\n\tPromise<Awaited<TResult>>\n> {\n\t// `_zod`, not `instanceof`: a schema from another copy of zod is still one.\n\tconst checked = (\n\t\t'_zod' in schema ? schema : z.object(schema as z.ZodRawShape)\n\t) as z.ZodType;\n\treturn async (source, args, context, info): Promise<Awaited<TResult>> => {\n\t\tconst where = `${info.parentType.name}.${info.fieldName}`;\n\t\tconst parsed = (await parseArgs(checked, args, where)) as z.output<\n\t\t\tSchemaOf<S>\n\t\t>;\n\t\treturn await resolve(source, parsed, context, info);\n\t};\n}\n",
|
|
7
9
|
"import {\n\tdefaultFieldResolver,\n\ttype GraphQLField,\n\ttype GraphQLFieldResolver,\n\ttype GraphQLSchema,\n} from 'graphql';\nimport type { z } from 'zod';\nimport { parseArgs } from './bad-user-input';\nimport { checkConstraints } from './builder/check-constraints';\nimport type { FormatSchemas, OwnFormats } from './formats/registry';\nimport { constraintOf } from './rules';\n\n// Symbol.for: two copies of the package in one tree still wrap a field once.\nconst wrapped = Symbol.for('@nxgt/graphql-validation/wrapped');\n// The formats a schema was wrapped with, kept on the schema itself.\nconst wrappedWith = Symbol.for('@nxgt/graphql-validation/formats');\n\n/** What `withValidation` takes besides the schema. */\nexport interface WithValidationOptions<\n\tF extends FormatSchemas = FormatSchemas,\n> {\n\t/**\n\t * The application's own formats, which `@constraint(format: \"...\")` may\n\t * name beside the built-in ones: one Zod string schema each, keyed by its\n\t * name (lowercase letters, digits and hyphens). A built-in name is a type\n\t * error, and refused at startup.\n\t */\n\treadonly formats?: OwnFormats<F>;\n}\n\ntype Resolver = GraphQLFieldResolver<unknown, unknown> & { [wrapped]?: true };\n\nfunction checking(\n\tschema: z.ZodType,\n\twhere: string,\n\tresolve: Resolver,\n): Resolver {\n\tconst check: Resolver = async (source, args, context, info) =>\n\t\tawait resolve(\n\t\t\tsource,\n\t\t\tawait parseArgs(schema, args, where, constraintOf),\n\t\t\tcontext,\n\t\t\tinfo,\n\t\t);\n\tcheck[wrapped] = true;\n\treturn check;\n}\n\n/**\n * Wraps a field once. A subscription field is checked in `subscribe`, which\n * runs once per subscription — graphql's default one when the field has none\n * — and its `resolve` runs once per event, with the same arguments, already\n * checked.\n */\nfunction wrap(\n\tfield: GraphQLField<unknown, unknown>,\n\targs: z.ZodType,\n\twhere: string,\n\tsubscription: boolean,\n): void {\n\tconst subscribe = (field.subscribe ??\n\t\t(subscription ? defaultFieldResolver : undefined)) as Resolver | undefined;\n\tif (subscribe) {\n\t\tif (!subscribe[wrapped]) field.subscribe = checking(args, where, subscribe);\n\t\treturn;\n\t}\n\tconst resolve = (field.resolve ?? defaultFieldResolver) as Resolver;\n\tif (!resolve[wrapped]) field.resolve = checking(args, where, resolve);\n}\n\n/**\n * Checks every `@constraint` of `schema` before the resolvers run. Each\n * field with a constrained argument — directly, or anywhere inside an input\n * type it takes — gets its resolver (and its `subscribe`) wrapped: the\n * arguments are parsed by the Zod schema the directives describe, the\n * resolver receives the parsed value, and an invalid input becomes one\n * `BAD_USER_INPUT` error. Fields with nothing to check are left untouched.\n *\n * The schema is built here, once: a constraint that cannot apply (a\n * `minLength` on an `Int`, an unknown format, a bad pattern) throws now, not\n * on the first request. The resolvers are wrapped in place and the same\n * schema is returned, so it goes wherever it went before; calling it twice\n * wraps nothing twice. Call it last, once every resolver is attached: a\n * resolver set on a field afterwards replaces the check.\n *\n * A `@constraint` on an interface's argument must be repeated on each\n * object's field, which is the one that resolves; a field that drops it\n * fails here.\n *\n * The directives are read from the SDL (`astNode`), so a schema built from\n * type definitions — `buildSchema`, `makeExecutableSchema`, Yoga, Apollo —\n * is checked; a code-first schema with no SDL has no `@constraint` to read.\n *\n * `options.formats` adds the application's own formats; wrapping a schema\n * again throws unless the record has the same names, each the very same\n * schema (no formats after some, or some after none, throws too), since its\n * fields are already checked against the first ones.\n */\nexport function withValidation<\n\tS extends GraphQLSchema,\n\t// biome-ignore lint/complexity/noBannedTypes: no formats of its own by default\n\tconst F extends FormatSchemas = {},\n>(schema: S, options: WithValidationOptions<F> = {}): S {\n\tconst formats: FormatSchemas = options.formats ?? {};\n\tconst holder = schema as unknown as { [wrappedWith]?: FormatSchemas };\n\tconst before = holder[wrappedWith];\n\tif (before && !sameFormats(before, formats)) {\n\t\tthrow new Error(\n\t\t\t'withValidation: this schema is already wrapped with other formats. Call withValidation once, with every format.',\n\t\t);\n\t}\n\tconst directive = checkConstraints(\n\t\tschema,\n\t\t(type, field, args) => {\n\t\t\tconst subscription = type === schema.getSubscriptionType();\n\t\t\twrap(field, args, `${type.name}.${field.name}`, subscription);\n\t\t},\n\t\tformats,\n\t);\n\tif (!directive) {\n\t\tthrow new Error(\n\t\t\t'withValidation: this schema declares no @constraint directive. Add constraintTypeDefs to its type definitions.',\n\t\t);\n\t}\n\tif (!before) Object.defineProperty(schema, wrappedWith, { value: formats });\n\treturn schema;\n}\n\n/** The same names, each the very same schema. */\nfunction sameFormats(a: FormatSchemas, b: FormatSchemas): boolean {\n\tconst names = Object.keys(a);\n\treturn (\n\t\tnames.length === Object.keys(b).length &&\n\t\tnames.every((name) => Object.hasOwn(b, name) && a[name] === b[name])\n\t);\n}\n"
|
|
8
10
|
],
|
|
9
|
-
"mappings": ";;;;;;;;;AAAA;AA8BA,SAAS,QAAQ,CAChB,OACA,cACoB;AAAA,EACpB,OAAO,MAAM,OAAO,IAAI,CAAC,UAAU;AAAA,IAClC,MAAM,OAAO,MAAM,KAAK,IAAI,CAAC,QAC5B,OAAO,QAAQ,WAAW,MAAM,OAAO,GAAG,CAC3C;AAAA,IACA,MAAM,aAAa,eAAe,KAAK;AAAA,IACvC,OAAO,aACJ,EAAE,MAAM,SAAS,MAAM,SAAS,MAAM,MAAM,MAAM,WAAW,IAC7D,EAAE,MAAM,SAAS,MAAM,SAAS,MAAM,MAAM,KAAK;AAAA,GACpD;AAAA;AAGF,SAAS,OAAO,CAAC,OAAe,QAAyC;AAAA,EACxE,OAAO,SAAS;AAAA,EAChB,MAAM,SAAS,QACZ,GAAG,MAAM,KAAK,KAAK,GAAG,KAAK,gBAAgB,MAAM,YACjD;AAAA,EACH,MAAM,aAAqC,EAAE,MAAM,kBAAkB,OAAO;AAAA,EAC5E,OAAO,IAAI,aAAa,yBAAyB,UAAU,UAAU;AAAA,IACpE;AAAA,EACD,CAAC;AAAA;AAUK,SAAS,YAAY,CAAC,OAAe,OAAiC;AAAA,EAC5E,OAAO,QAAQ,OAAO,SAAS,KAAK,CAAC;AAAA;AAItC,eAAsB,SAAS,CAC9B,QACA,MACA,OACA,cACmB;AAAA,EACnB,MAAM,SAAS,MAAM,OAAO,eAAe,IAAI;AAAA,EAC/C,IAAI,CAAC,OAAO;AAAA,IACX,MAAM,QAAQ,OAAO,SAAS,OAAO,OAAO,YAAY,CAAC;AAAA,EAC1D,OAAO,OAAO;AAAA;;
|
|
10
|
-
"debugId": "
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "BC885DEBB67481A464756E2164756E21",
|
|
11
13
|
"names": []
|
|
12
14
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { GraphQLError, type GraphQLErrorOptions } from 'graphql';
|
|
2
|
+
/** The code of the error `invalidInputError` builds, and its default message. */
|
|
3
|
+
export declare const INVALID_INPUT = "validation.errors.invalid-input";
|
|
4
|
+
/** Values a message is built from, such as `{ min: 2 }`. */
|
|
5
|
+
export type IssueParams = Record<string, string | number>;
|
|
6
|
+
/** One field at fault: where it is, why, and what the reason's message needs. */
|
|
7
|
+
export interface FieldIssue {
|
|
8
|
+
/** The field inside the input, dotted: `person.firstName`, `tags.1`. */
|
|
9
|
+
readonly path: string;
|
|
10
|
+
/** A code your client and your translations know: `validation.errors.too-short`. */
|
|
11
|
+
readonly code: string;
|
|
12
|
+
/** Values the message needs; an object with no key counts as none. */
|
|
13
|
+
readonly params?: IssueParams;
|
|
14
|
+
}
|
|
15
|
+
/** One entry of `extensions.fields`: a `FieldIssue` with its message. */
|
|
16
|
+
export interface FieldError {
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly code: string;
|
|
19
|
+
/** `translate(code, params)`, or the code itself without a translator. */
|
|
20
|
+
readonly message: string;
|
|
21
|
+
/** Present only when the issue had params with at least one key. */
|
|
22
|
+
readonly params?: IssueParams;
|
|
23
|
+
}
|
|
24
|
+
/** Turns a code (and its params) into the text a person reads. */
|
|
25
|
+
export type Translate = (code: string, params?: IssueParams) => string;
|
|
26
|
+
/** The options of `invalidInputError`: GraphQL's own, and a translator. */
|
|
27
|
+
export type InvalidInputOptions = GraphQLErrorOptions & {
|
|
28
|
+
readonly translate?: Translate;
|
|
29
|
+
};
|
|
30
|
+
/** The options of `fieldsError`: also an HTTP status and the error's own params. */
|
|
31
|
+
export type FieldsErrorOptions = InvalidInputOptions & {
|
|
32
|
+
/** `extensions.http.status`, for a server that reads it (409, ...). */
|
|
33
|
+
readonly status?: number;
|
|
34
|
+
readonly params?: IssueParams;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* One 400 error listing every field at fault, for an API that answers invalid
|
|
38
|
+
* input this way: `extensions` is `{ code: 'validation.errors.invalid-input',
|
|
39
|
+
* http: { status: 400 }, fields }`, each field with its translated message
|
|
40
|
+
* (the code itself without `translate`), in the order given. GraphQL's
|
|
41
|
+
* options (`originalError`, `path`, `nodes`, ...) are kept; an `extensions`
|
|
42
|
+
* among them is replaced by this one.
|
|
43
|
+
*/
|
|
44
|
+
export declare function invalidInputError(issues: readonly FieldIssue[], options?: InvalidInputOptions): GraphQLError;
|
|
45
|
+
/**
|
|
46
|
+
* A refusal of your own (a 409 conflict, say) that names the fields at
|
|
47
|
+
* fault: `extensions` is `{ code, params?, http?: { status }, fields }`, the
|
|
48
|
+
* fields worded as `invalidInputError` words them, the message
|
|
49
|
+
* `translate(code, params)` or the code. An `extensions` among the options
|
|
50
|
+
* is replaced by this one.
|
|
51
|
+
*/
|
|
52
|
+
export declare function fieldsError(code: string, issues: readonly FieldIssue[], options?: FieldsErrorOptions): GraphQLError;
|
|
53
|
+
/**
|
|
54
|
+
* A GraphQL or Zod path as a `FieldIssue` path: dotted, without the leading
|
|
55
|
+
* `input` argument when a field follows it, a symbol written as
|
|
56
|
+
* `String(key)`. `['input', 'person', 'firstName']` is `person.firstName`,
|
|
57
|
+
* `['input', 'tags', 1]` is `tags.1`, `['first']` and `['input']` stay as
|
|
58
|
+
* they are.
|
|
59
|
+
*/
|
|
60
|
+
export declare function inputPath(path: readonly PropertyKey[]): string;
|
|
61
|
+
//# sourceMappingURL=invalid-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalid-input.d.ts","sourceRoot":"","sources":["../src/invalid-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEjE,iFAAiF;AACjF,eAAO,MAAM,aAAa,oCAAoC,CAAC;AAE/D,4DAA4D;AAC5D,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAE1D,iFAAiF;AACjF,MAAM,WAAW,UAAU;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC9B;AAED,yEAAyE;AACzE,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC9B;AAED,kEAAkE;AAClE,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,MAAM,CAAC;AAEvE,2EAA2E;AAC3E,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,GAAG;IACvD,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAC/B,CAAC;AAEF,oFAAoF;AACpF,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,GAAG;IACtD,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC9B,CAAC;AA8BF;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,OAAO,GAAE,mBAAwB,GAC/B,YAAY,CAUd;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,OAAO,GAAE,kBAAuB,GAC9B,YAAY,CAYd;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAK9D"}
|
package/docs/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
| Page | Read it when |
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| [Constraints](guide/constraints.md) | you write `@constraint` in your schema: setup with `withValidation`, every argument and its Zod equivalent, the formats and your own, lists and nested inputs |
|
|
8
|
+
| [Answering invalid input as one error](guide/invalid-input.md) | your API answers invalid input as one 400 error with a list of fields (`invalidInputError`, `fieldIssueOf` for a Zod parse), or a refusal such as a 409 names the fields at fault (`fieldsError`) |
|
|
8
9
|
| [Errors](guide/errors.md) | you handle the `BAD_USER_INPUT` error in a client, build it yourself with `badUserInput`, or add rules a directive cannot say with `validated` |
|
|
9
10
|
|
|
10
11
|
## Reference
|
|
@@ -145,6 +145,11 @@ for that.
|
|
|
145
145
|
`minItems` and `maxItems` apply to the list; every other rule applies to its
|
|
146
146
|
items. A nullable argument also accepts `null` and absence.
|
|
147
147
|
|
|
148
|
+
A list longer than `maxItems` is refused with its size alone from zod 4.6.6
|
|
149
|
+
on: Zod stops an oversized array before its items (its `capSize`), so the
|
|
150
|
+
error has one `too_big` issue at the list's path and none for its items.
|
|
151
|
+
With zod 4.6.5, the items' issues come first, then the `too_big`.
|
|
152
|
+
|
|
148
153
|
```graphql
|
|
149
154
|
type Query {
|
|
150
155
|
# at least one tag, at most 5; each tag 1 to 20 characters
|
package/docs/guide/errors.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
The error an invalid input becomes, how a client reads it, and `validated` for
|
|
4
4
|
rules a `@constraint` cannot say. For declaring constraints, see
|
|
5
|
-
[Constraints](constraints.md).
|
|
5
|
+
[Constraints](constraints.md). For an API that answers invalid input as one
|
|
6
|
+
400 error with a list of fields, see
|
|
7
|
+
[Answering invalid input as one error](invalid-input.md).
|
|
6
8
|
|
|
7
9
|
## The error
|
|
8
10
|
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Answering invalid input as one error
|
|
2
|
+
|
|
3
|
+
Some APIs answer invalid input as **one 400 error with a list of fields**,
|
|
4
|
+
each with a code a client translates or a message already translated, rather
|
|
5
|
+
than as [`BAD_USER_INPUT`](errors.md) with Zod's issues. `invalidInputError`
|
|
6
|
+
and `fieldsError` build that error, so every resolver and every module words
|
|
7
|
+
it the same way. They do not change what `withValidation` and `validated`
|
|
8
|
+
raise: those still throw `BAD_USER_INPUT`.
|
|
9
|
+
|
|
10
|
+
## The error
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { invalidInputError } from '@nxgt/graphql-validation';
|
|
14
|
+
|
|
15
|
+
throw invalidInputError(
|
|
16
|
+
[
|
|
17
|
+
{ path: 'person.lastName', code: 'validation.errors.required' },
|
|
18
|
+
{ path: 'person.firstName', code: 'validation.errors.too-short', params: { min: 2 } },
|
|
19
|
+
],
|
|
20
|
+
{ translate: (code, params) => t(code, params) }, // your i18n function
|
|
21
|
+
);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"errors": [
|
|
27
|
+
{
|
|
28
|
+
"message": "Invalid input",
|
|
29
|
+
"path": ["createPerson"],
|
|
30
|
+
"extensions": {
|
|
31
|
+
"code": "validation.errors.invalid-input",
|
|
32
|
+
"http": { "status": 400 },
|
|
33
|
+
"fields": [
|
|
34
|
+
{ "path": "person.lastName", "code": "validation.errors.required", "message": "Required" },
|
|
35
|
+
{ "path": "person.firstName", "code": "validation.errors.too-short", "message": "At least 2 characters", "params": { "min": 2 } }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"data": { "createPerson": null }
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `message` is `translate('validation.errors.invalid-input')`, or that code
|
|
45
|
+
itself without `translate` (also exported as `INVALID_INPUT`).
|
|
46
|
+
- `extensions.code` is `validation.errors.invalid-input`, and
|
|
47
|
+
`extensions.http.status` is `400`, for a server that sets the response's
|
|
48
|
+
HTTP status from it (GraphQL Yoga and Apollo Server read this key).
|
|
49
|
+
- `extensions.fields` holds one entry per issue, **in the order given**
|
|
50
|
+
(duplicates kept): `path` and `code` as you wrote them, `message` as
|
|
51
|
+
`translate(code, params)` or the code itself, and `params` only when the
|
|
52
|
+
issue has some.
|
|
53
|
+
- **`params` is never `{}`**: an issue whose `params` has no key is written
|
|
54
|
+
without it, and `translate` receives `undefined` for it.
|
|
55
|
+
- A `translate` that returns `undefined` at run time (a missing key in some
|
|
56
|
+
i18n libraries) falls back to the code, as if there were none.
|
|
57
|
+
|
|
58
|
+
## GraphQL's options
|
|
59
|
+
|
|
60
|
+
The second argument also takes `GraphQLError`'s own options, so the error
|
|
61
|
+
keeps its cause and its place in the response:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
throw invalidInputError(issues, {
|
|
65
|
+
translate,
|
|
66
|
+
originalError: zodError, // kept: error.originalError
|
|
67
|
+
path: info.path ? [info.path.key] : undefined,
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**`extensions` among them is replaced**, not merged: the error's
|
|
72
|
+
`extensions` is always exactly the shape above. Put anything else your
|
|
73
|
+
server needs in a `formatError` hook.
|
|
74
|
+
|
|
75
|
+
## From a resolver
|
|
76
|
+
|
|
77
|
+
A check that needs the database, or a rule your schema cannot say, ends with
|
|
78
|
+
the same error:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { type FieldIssue, invalidInputError } from '@nxgt/graphql-validation';
|
|
82
|
+
|
|
83
|
+
const resolvers = {
|
|
84
|
+
Mutation: {
|
|
85
|
+
async createPerson(_: unknown, { input }: { input: PersonInput }, { t }: Context) {
|
|
86
|
+
const issues: FieldIssue[] = [];
|
|
87
|
+
if (!input.person.lastName.trim())
|
|
88
|
+
issues.push({ path: 'person.lastName', code: 'validation.errors.required' });
|
|
89
|
+
if (input.person.birthDate > new Date())
|
|
90
|
+
issues.push({ path: 'person.birthDate', code: 'validation.errors.in-the-future' });
|
|
91
|
+
if (issues.length) throw invalidInputError(issues, { translate: t });
|
|
92
|
+
return people.create(input.person);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## From a zod parse
|
|
99
|
+
|
|
100
|
+
`fieldIssueOf(issue)` maps one Zod issue to a `FieldIssue`: its path through
|
|
101
|
+
`inputPath`, and a code under `validation.errors.`, without params:
|
|
102
|
+
|
|
103
|
+
| Zod issue | `code` |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| a string too short or too long (`min`, `max`, `length`) | `validation.errors.too-short` / `validation.errors.too-long` |
|
|
106
|
+
| a number or bigint out of its bounds, or not a multiple | `validation.errors.out-of-range` |
|
|
107
|
+
| a list or set too small or too big | `validation.errors.too-few` / `validation.errors.too-many` |
|
|
108
|
+
| a named format (`email`, `uuid`, …) or a pattern (`regex`) | `validation.errors.invalid-format` |
|
|
109
|
+
| anything else: `startsWith`, `endsWith`, `includes`, a refinement, a wrong type, a date's bound | `validation.errors.invalid` |
|
|
110
|
+
|
|
111
|
+
The codes are also exported as `FIELD_CODES` (`FIELD_CODES.tooShort`, …),
|
|
112
|
+
for your translations.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { fieldIssueOf, invalidInputError } from '@nxgt/graphql-validation';
|
|
116
|
+
import { z } from 'zod';
|
|
117
|
+
|
|
118
|
+
const PersonInput = z.object({
|
|
119
|
+
person: z.object({
|
|
120
|
+
firstName: z.string().min(2),
|
|
121
|
+
email: z.email(),
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = PersonInput.safeParse(input);
|
|
126
|
+
if (!result.success)
|
|
127
|
+
throw invalidInputError(result.error.issues.map(fieldIssueOf), {
|
|
128
|
+
translate: t,
|
|
129
|
+
originalError: result.error,
|
|
130
|
+
});
|
|
131
|
+
// fields: person.firstName (validation.errors.too-short),
|
|
132
|
+
// person.email (validation.errors.invalid-format)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
[`@nxgt/graphql-codegen-msw`](https://github.com/softistx/nxgt-graphql/blob/develop/packages/graphql-codegen-msw/docs/guide/handlers.md)'s
|
|
136
|
+
mocks apply the same mapping by default, so a mocked refusal reads as the
|
|
137
|
+
server's. For a code of your own, or params (`{ min: 2 }`), map the issue
|
|
138
|
+
yourself and keep `fieldIssueOf` for the rest:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { type FieldIssue, fieldIssueOf } from '@nxgt/graphql-validation';
|
|
142
|
+
|
|
143
|
+
const issueOf = (issue: z.core.$ZodIssue): FieldIssue =>
|
|
144
|
+
issue.code === 'too_small' && issue.origin === 'string'
|
|
145
|
+
? { ...fieldIssueOf(issue), params: { min: Number(issue.minimum) } }
|
|
146
|
+
: fieldIssueOf(issue);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`inputPath` turns a GraphQL or Zod path into a dotted one, dropping the
|
|
150
|
+
leading `input` argument: `inputPath(['input', 'person', 'firstName'])` is
|
|
151
|
+
`person.firstName`, `['input', 'tags', 1]` is `tags.1`; a path that does not
|
|
152
|
+
start with `input` (`['first']`), or is `['input']` alone, is only joined
|
|
153
|
+
with dots. It takes a Zod issue's path as it is, a symbol key written as
|
|
154
|
+
`String(key)`.
|
|
155
|
+
|
|
156
|
+
The same mapping reads [`BAD_USER_INPUT`](errors.md)'s `issues[].path`, which
|
|
157
|
+
starts with the argument name.
|
|
158
|
+
|
|
159
|
+
## A refusal that names fields: `fieldsError`
|
|
160
|
+
|
|
161
|
+
For a refusal of your own that is not a 400, a 409 conflict for instance,
|
|
162
|
+
that still points at the fields at fault:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { fieldsError } from '@nxgt/graphql-validation';
|
|
166
|
+
|
|
167
|
+
throw fieldsError(
|
|
168
|
+
'person.errors.duplicate',
|
|
169
|
+
[{ path: 'person.email', code: 'person.errors.email-taken' }],
|
|
170
|
+
{ translate: t, status: 409, params: { name: existing.name } },
|
|
171
|
+
);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"message": "Already registered as Ada",
|
|
177
|
+
"extensions": {
|
|
178
|
+
"code": "person.errors.duplicate",
|
|
179
|
+
"params": { "name": "Ada" },
|
|
180
|
+
"http": { "status": 409 },
|
|
181
|
+
"fields": [
|
|
182
|
+
{ "path": "person.email", "code": "person.errors.email-taken", "message": "This email is taken" }
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- `message` is `translate(code, params)`, or `code`.
|
|
189
|
+
- `extensions` is `{ code, params?, http?, fields }`, in that order:
|
|
190
|
+
`params` only when it has a key, `http: { status }` only when `status` is
|
|
191
|
+
given, `fields` worded as `invalidInputError` words them.
|
|
192
|
+
- GraphQL's options are kept and an `extensions` among them is replaced, as
|
|
193
|
+
above.
|
|
194
|
+
|
|
195
|
+
## Types
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import type { GraphQLError, GraphQLErrorOptions } from 'graphql';
|
|
199
|
+
import type { z } from 'zod';
|
|
200
|
+
|
|
201
|
+
type IssueParams = Record<string, string | number>;
|
|
202
|
+
|
|
203
|
+
interface FieldIssue {
|
|
204
|
+
readonly path: string;
|
|
205
|
+
readonly code: string;
|
|
206
|
+
readonly params?: IssueParams; // {} counts as none
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** One entry of `extensions.fields`. */
|
|
210
|
+
interface FieldError {
|
|
211
|
+
readonly path: string;
|
|
212
|
+
readonly code: string;
|
|
213
|
+
readonly message: string;
|
|
214
|
+
readonly params?: IssueParams;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
type Translate = (code: string, params?: IssueParams) => string;
|
|
218
|
+
|
|
219
|
+
type InvalidInputOptions = GraphQLErrorOptions & { readonly translate?: Translate };
|
|
220
|
+
type FieldsErrorOptions = InvalidInputOptions & {
|
|
221
|
+
readonly status?: number;
|
|
222
|
+
readonly params?: IssueParams;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
declare const INVALID_INPUT: 'validation.errors.invalid-input';
|
|
226
|
+
declare function invalidInputError(issues: readonly FieldIssue[], options?: InvalidInputOptions): GraphQLError;
|
|
227
|
+
declare function fieldsError(code: string, issues: readonly FieldIssue[], options?: FieldsErrorOptions): GraphQLError;
|
|
228
|
+
declare function inputPath(path: readonly PropertyKey[]): string;
|
|
229
|
+
|
|
230
|
+
declare const FIELD_CODES: {
|
|
231
|
+
readonly tooShort: 'validation.errors.too-short';
|
|
232
|
+
readonly tooLong: 'validation.errors.too-long';
|
|
233
|
+
readonly outOfRange: 'validation.errors.out-of-range';
|
|
234
|
+
readonly tooFew: 'validation.errors.too-few';
|
|
235
|
+
readonly tooMany: 'validation.errors.too-many';
|
|
236
|
+
readonly invalidFormat: 'validation.errors.invalid-format';
|
|
237
|
+
readonly invalid: 'validation.errors.invalid';
|
|
238
|
+
};
|
|
239
|
+
declare function fieldIssueOf(issue: z.core.$ZodIssue): FieldIssue;
|
|
240
|
+
```
|
package/docs/roadmap.md
CHANGED
|
@@ -4,19 +4,35 @@ What `@nxgt/graphql-validation` is heading for, phrased as what you get.
|
|
|
4
4
|
|
|
5
5
|
## Now
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
`@constraint(format: "...")`: `withValidation(schema, { formats })`, one
|
|
9
|
-
record your module exports, checked at startup, its refusals named
|
|
10
|
-
`format`. `@nxgt/graphql-codegen-zod` 0.3 reads the same record, so the
|
|
11
|
-
client refuses what the server refuses, with the same messages.
|
|
7
|
+
Nothing in progress.
|
|
12
8
|
|
|
13
9
|
## Next
|
|
14
10
|
|
|
15
11
|
- **Schemas that read the context**, `(args, context) => schema`, for checks
|
|
16
12
|
that need the request (uniqueness in a database, permissions).
|
|
17
13
|
|
|
14
|
+
## Later
|
|
15
|
+
|
|
16
|
+
- **Emit the invalid-input shape directly.** An option for `withValidation`
|
|
17
|
+
and `validated` to raise the `invalidInputError` shape in place of
|
|
18
|
+
`BAD_USER_INPUT`, so an API using it needs no mapping of its own.
|
|
19
|
+
|
|
18
20
|
## Shipped
|
|
19
21
|
|
|
22
|
+
- **Answering invalid input as one error** — 0.4.0.
|
|
23
|
+
`invalidInputError(issues, { translate })` builds one 400 error whose
|
|
24
|
+
`extensions.fields` lists every field at fault with your own codes,
|
|
25
|
+
translated; `fieldsError` does the same for a refusal such as a 409
|
|
26
|
+
conflict; `fieldIssueOf` maps a Zod issue to a field and a code
|
|
27
|
+
(`too-short`, `invalid-format`, …), the mapping
|
|
28
|
+
[`@nxgt/graphql-codegen-msw`](https://www.npmjs.com/package/@nxgt/graphql-codegen-msw)'s
|
|
29
|
+
mocks apply; `inputPath` turns a GraphQL or Zod path into `person.firstName`.
|
|
30
|
+
- **Your own formats** — 0.3.0. Name a Zod string schema in
|
|
31
|
+
`@constraint(format: "...")`: `withValidation(schema, { formats })`, one
|
|
32
|
+
record your module exports, checked at startup, its refusals named
|
|
33
|
+
`format`. `@nxgt/graphql-codegen-zod` 0.3 reads the same record, so the
|
|
34
|
+
client refuses what the server refuses, with the same messages. From
|
|
35
|
+
0.3.1 they run through Zod's public API only.
|
|
20
36
|
- **Declarations resolve under `nodenext`** — 0.2.1. `moduleResolution`
|
|
21
37
|
`nodenext` and `node16` find every export, as `bundler` does.
|
|
22
38
|
- **A Zod codegen plugin** — 0.2.0. [`@nxgt/graphql-codegen-zod`](https://www.npmjs.com/package/@nxgt/graphql-codegen-zod)
|
package/package.json
CHANGED