@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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAG7B;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC;AAE5D,uCAAuC;AACvC,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAEb,EACJ,QAAQ,EAAE,CAAC,IAAI,kBAAkB,GAAG,OAAO,CAAC,cAAc,EAAE;IAAE,QAAQ,EAAE,CAAC,CAAA;CAAE,CAAC,GAC5E,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,SAAS,CACxB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,CAAC,CAAC,OAAO,EACjB,KAAK,EAAE,MAAM,GAAG,MAAM,GACpB,CAAC,CAAC,OAAO,CAMX;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAC5B,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,MAAM,GACpB,MAAM,CAMR;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAC3B,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,GACrB,kBAAkB,GAAG,SAAS,CAGhC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-items.d.ts","sourceRoot":"","sources":["../../src/rules/max-items.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,kDAOvB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-length.d.ts","sourceRoot":"","sources":["../../src/rules/max-length.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa,qDAOxB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max.d.ts","sourceRoot":"","sources":["../../src/rules/max.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,OAAO,iDAUlB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"min-items.d.ts","sourceRoot":"","sources":["../../src/rules/min-items.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,kDAOvB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"min-length.d.ts","sourceRoot":"","sources":["../../src/rules/min-length.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa,qDAOxB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"min.d.ts","sourceRoot":"","sources":["../../src/rules/min.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,OAAO,iDAUlB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multiple-of.d.ts","sourceRoot":"","sources":["../../src/rules/multiple-of.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,wDAOzB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"not-contains.d.ts","sourceRoot":"","sources":["../../src/rules/not-contains.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,eAAe,0DAU1B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pattern.d.ts","sourceRoot":"","sources":["../../src/rules/pattern.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,WAAW,sDAQtB,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { StringSchema } from '../formats/format';
|
|
3
|
+
/** The schema each kind of rule narrows. */
|
|
4
|
+
export interface Targets {
|
|
5
|
+
string: StringSchema;
|
|
6
|
+
number: z.ZodNumber;
|
|
7
|
+
list: z.ZodArray<z.ZodType>;
|
|
8
|
+
}
|
|
9
|
+
export type Target = keyof Targets;
|
|
10
|
+
/** What a `@constraint` argument's value can be. */
|
|
11
|
+
interface ValueOf {
|
|
12
|
+
Int: number;
|
|
13
|
+
Float: number;
|
|
14
|
+
String: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* One `@constraint` argument: its GraphQL type in the directive's SDL, the
|
|
18
|
+
* kind of value it narrows, and the same narrowing twice — `toZod` applied to
|
|
19
|
+
* a schema at startup, `toCode` written as source by a code generator. The
|
|
20
|
+
* spec beside each rule proves the two accept and refuse the same values.
|
|
21
|
+
*
|
|
22
|
+
* A `base` rule (only `format`) replaces the schema instead of narrowing it,
|
|
23
|
+
* so a builder applies it before every other rule of its target.
|
|
24
|
+
*/
|
|
25
|
+
export interface Rule<A extends string = string, T extends keyof ValueOf = keyof ValueOf, K extends Target = Target> {
|
|
26
|
+
readonly argument: A;
|
|
27
|
+
readonly type: T;
|
|
28
|
+
readonly target: K;
|
|
29
|
+
readonly base?: true;
|
|
30
|
+
readonly toZod: (schema: Targets[K], value: ValueOf[T]) => Targets[K];
|
|
31
|
+
/**
|
|
32
|
+
* `schema` is the source of the schema this rule narrows. The source names
|
|
33
|
+
* zod as a free `z`: the generated file imports it.
|
|
34
|
+
*/
|
|
35
|
+
readonly toCode: (schema: string, value: ValueOf[T]) => string;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a Zod issue is this rule's refusal, so the error a client gets
|
|
38
|
+
* names the rule (`constraint: 'minLength'`), which does not change with
|
|
39
|
+
* Zod's own issue codes.
|
|
40
|
+
*/
|
|
41
|
+
readonly owns: (issue: IssueFields) => boolean;
|
|
42
|
+
}
|
|
43
|
+
/** The fields of a Zod issue a rule recognises its refusal by. */
|
|
44
|
+
export interface IssueFields {
|
|
45
|
+
readonly code: string;
|
|
46
|
+
readonly origin?: string;
|
|
47
|
+
readonly format?: string;
|
|
48
|
+
readonly inclusive?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Declares a rule, with its argument name kept literal. */
|
|
51
|
+
export declare function defineRule<const A extends string, T extends keyof ValueOf, K extends Target>(rule: Rule<A, T, K>): Rule<A, T, K>;
|
|
52
|
+
/** A string literal as source: quotes and escapes included. */
|
|
53
|
+
export declare const literal: (value: string) => string;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=rule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.d.ts","sourceRoot":"","sources":["../../src/rules/rule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,4CAA4C;AAC5C,MAAM,WAAW,OAAO;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;CAC5B;AAED,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;AAEnC,oDAAoD;AACpD,UAAU,OAAO;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,IAAI,CACpB,CAAC,SAAS,MAAM,GAAG,MAAM,EACzB,CAAC,SAAS,MAAM,OAAO,GAAG,MAAM,OAAO,EACvC,CAAC,SAAS,MAAM,GAAG,MAAM;IAEzB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACtE;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC;CAC/C;AAED,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,4DAA4D;AAC5D,wBAAgB,UAAU,CACzB,KAAK,CAAC,CAAC,SAAS,MAAM,EACtB,CAAC,SAAS,MAAM,OAAO,EACvB,CAAC,SAAS,MAAM,EACf,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAEpC;AAED,+DAA+D;AAC/D,eAAO,MAAM,OAAO,GAAI,OAAO,MAAM,KAAG,MAA+B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"starts-with.d.ts","sourceRoot":"","sources":["../../src/rules/starts-with.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,yDAOzB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Where `--out` writes when it is given no file. */
|
|
2
|
+
export declare const DEFAULT_OUT = "generated/graphql/constraint.graphqls";
|
|
3
|
+
/** The `.graphqls` file's content: a header, then the SDL. */
|
|
4
|
+
export declare function graphqls(): string;
|
|
5
|
+
/**
|
|
6
|
+
* Runs the bin on `argv`: 0 done, 1 the file could not be written, 2 a usage
|
|
7
|
+
* error (nxgt-mongo-backup's convention). `--help` anywhere wins.
|
|
8
|
+
*/
|
|
9
|
+
export declare function main(argv: readonly string[]): Promise<number>;
|
|
10
|
+
//# sourceMappingURL=typedefs-command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typedefs-command.d.ts","sourceRoot":"","sources":["../src/typedefs-command.ts"],"names":[],"mappings":"AAOA,qDAAqD;AACrD,eAAO,MAAM,WAAW,0CAA0C,CAAC;AAUnE,8DAA8D;AAC9D,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAOD;;;GAGG;AACH,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA+BnE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/** An object schema of the arguments, or its shape. */
|
|
4
|
+
export type ArgsSchema = z.ZodType<Record<string, unknown>> | z.ZodRawShape;
|
|
5
|
+
/** The schema `ArgsSchema` stands for. */
|
|
6
|
+
export type SchemaOf<S extends ArgsSchema> = S extends z.ZodType ? S : z.ZodObject<S & z.ZodRawShape>;
|
|
7
|
+
/**
|
|
8
|
+
* A resolver whose arguments are checked by `schema` before it runs, for what
|
|
9
|
+
* a `@constraint` cannot say: a rule across arguments, a business refinement,
|
|
10
|
+
* an async check. It receives the parsed arguments (`z.output`), defaults and
|
|
11
|
+
* transforms applied; an invalid input is the same `BAD_USER_INPUT` error as
|
|
12
|
+
* the directives'.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* signUp: validated(
|
|
16
|
+
* z.object({ input: signUpSchema }).refine(({ input }) => input.password !== input.email),
|
|
17
|
+
* (_, { input }) => users.create(input),
|
|
18
|
+
* )
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function validated<S extends ArgsSchema, TSource = unknown, TContext = unknown, TResult = unknown>(schema: S, resolve: (source: TSource, args: z.output<SchemaOf<S>>, context: TContext, info: GraphQLResolveInfo) => TResult): GraphQLFieldResolver<TSource, TContext, z.input<SchemaOf<S>>, Promise<Awaited<TResult>>>;
|
|
22
|
+
//# sourceMappingURL=validated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validated.d.ts","sourceRoot":"","sources":["../src/validated.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,uDAAuD;AACvD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC;AAE5E,0CAA0C;AAC1C,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,UAAU,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,GAC7D,CAAC,GACD,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC;AAElC;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CACxB,CAAC,SAAS,UAAU,EACpB,OAAO,GAAG,OAAO,EACjB,QAAQ,GAAG,OAAO,EAClB,OAAO,GAAG,OAAO,EAEjB,MAAM,EAAE,CAAC,EACT,OAAO,EAAE,CACR,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,QAAQ,EACjB,IAAI,EAAE,kBAAkB,KACpB,OAAO,GACV,oBAAoB,CACtB,OAAO,EACP,QAAQ,EACR,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EACpB,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CACzB,CAYA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type GraphQLSchema } from 'graphql';
|
|
2
|
+
/**
|
|
3
|
+
* Checks every `@constraint` of `schema` before the resolvers run. Each
|
|
4
|
+
* field with a constrained argument — directly, or anywhere inside an input
|
|
5
|
+
* type it takes — gets its resolver (and its `subscribe`) wrapped: the
|
|
6
|
+
* arguments are parsed by the Zod schema the directives describe, the
|
|
7
|
+
* resolver receives the parsed value, and an invalid input becomes one
|
|
8
|
+
* `BAD_USER_INPUT` error. Fields with nothing to check are left untouched.
|
|
9
|
+
*
|
|
10
|
+
* The schema is built here, once: a constraint that cannot apply (a
|
|
11
|
+
* `minLength` on an `Int`, an unknown format, a bad pattern) throws now, not
|
|
12
|
+
* on the first request. The resolvers are wrapped in place and the same
|
|
13
|
+
* schema is returned, so it goes wherever it went before; calling it twice
|
|
14
|
+
* wraps nothing twice. Call it last, once every resolver is attached: a
|
|
15
|
+
* resolver set on a field afterwards replaces the check.
|
|
16
|
+
*
|
|
17
|
+
* A `@constraint` on an interface's argument must be repeated on each
|
|
18
|
+
* object's field, which is the one that resolves; a field that drops it
|
|
19
|
+
* fails here.
|
|
20
|
+
*
|
|
21
|
+
* The directives are read from the SDL (`astNode`), so a schema built from
|
|
22
|
+
* type definitions — `buildSchema`, `makeExecutableSchema`, Yoga, Apollo —
|
|
23
|
+
* is checked; a code-first schema with no SDL has no `@constraint` to read.
|
|
24
|
+
*/
|
|
25
|
+
export declare function withValidation<S extends GraphQLSchema>(schema: S): S;
|
|
26
|
+
//# sourceMappingURL=with-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-validation.d.ts","sourceRoot":"","sources":["../src/with-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,KAAK,aAAa,EAClB,MAAM,SAAS,CAAC;AAiDjB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,aAAa,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAWpE"}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @nxgt/graphql-validation documentation
|
|
2
|
+
|
|
3
|
+
## Guides
|
|
4
|
+
|
|
5
|
+
| Page | Read it when |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| [Constraints](guide/constraints.md) | you write `@constraint` in your schema: setup with `withValidation`, every argument and its Zod equivalent, the formats, lists and nested inputs |
|
|
8
|
+
| [Errors](guide/errors.md) | you handle the `BAD_USER_INPUT` error in a client, build it yourself with `badUserInput`, or add rules a directive cannot say with `validated` |
|
|
9
|
+
|
|
10
|
+
## Reference
|
|
11
|
+
|
|
12
|
+
| Page | Read it when |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| [Troubleshooting](troubleshooting.md) | you have an error message and want its cause |
|
|
15
|
+
| [Roadmap](roadmap.md) | you want to know what is coming, and what is not |
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Constraints
|
|
2
|
+
|
|
3
|
+
How to declare checks with `@constraint` and turn them on with
|
|
4
|
+
`withValidation`. For the error an invalid input becomes, see
|
|
5
|
+
[Errors](errors.md).
|
|
6
|
+
|
|
7
|
+
## Smallest complete example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { makeExecutableSchema } from '@graphql-tools/schema';
|
|
11
|
+
import { graphql } from 'graphql';
|
|
12
|
+
import { constraintTypeDefs, withValidation } from '@nxgt/graphql-validation';
|
|
13
|
+
|
|
14
|
+
const schema = withValidation(
|
|
15
|
+
makeExecutableSchema({
|
|
16
|
+
typeDefs: [
|
|
17
|
+
constraintTypeDefs,
|
|
18
|
+
/* GraphQL */ `
|
|
19
|
+
type Query {
|
|
20
|
+
greet(name: String! @constraint(minLength: 2)): String
|
|
21
|
+
}
|
|
22
|
+
`,
|
|
23
|
+
],
|
|
24
|
+
resolvers: { Query: { greet: (_, { name }) => `Hello ${name}` } },
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const result = await graphql({ schema, source: '{ greet(name: "A") }' });
|
|
29
|
+
// result.errors[0].message:
|
|
30
|
+
// "Invalid arguments for Query.greet. name: Too small: expected string to have >=2 characters"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## The flow
|
|
34
|
+
|
|
35
|
+
1. Put `constraintTypeDefs` in your type definitions. It is a string: the SDL
|
|
36
|
+
of the `@constraint` directive.
|
|
37
|
+
2. Write `@constraint(...)` on arguments and input fields.
|
|
38
|
+
3. Build the schema: `buildSchema`, `makeExecutableSchema`, GraphQL Yoga,
|
|
39
|
+
Apollo Server, anything that builds from type definitions.
|
|
40
|
+
4. Attach every resolver, then call `withValidation(schema)` **last**: a
|
|
41
|
+
resolver set on a field afterwards replaces the check.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { buildSchema } from 'graphql';
|
|
45
|
+
import { constraintTypeDefs, withValidation } from '@nxgt/graphql-validation';
|
|
46
|
+
|
|
47
|
+
const schema = buildSchema(`${constraintTypeDefs}
|
|
48
|
+
type Query { code(value: String @constraint(pattern: "^[A-Z]{3}$")): String }
|
|
49
|
+
`);
|
|
50
|
+
// ... set field resolvers on `schema` here ...
|
|
51
|
+
withValidation(schema);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
What `withValidation` does:
|
|
55
|
+
|
|
56
|
+
- It wraps the resolver of every field that has a constrained argument, directly
|
|
57
|
+
or anywhere inside an input type it takes: `subscribe` for a subscription
|
|
58
|
+
field, `resolve` otherwise.
|
|
59
|
+
- It wraps in place and returns the same schema, typed as you passed it.
|
|
60
|
+
Calling it twice wraps nothing twice.
|
|
61
|
+
- The resolver receives the parsed arguments (`z.output` of the schema the
|
|
62
|
+
directives describe). An argument or input field the client left out stays
|
|
63
|
+
**absent** from them, not `undefined`: `{ input: { email } }`, with
|
|
64
|
+
`Object.keys(input)` equal to `['email']`. A `@oneOf` input keeps its one key.
|
|
65
|
+
- Each issue it raises carries `constraint`, the `@constraint` argument that
|
|
66
|
+
refused (see [Errors](errors.md#the-error)).
|
|
67
|
+
- A default value that breaks its own constraint throws at the call, naming
|
|
68
|
+
the place: `The default value of Query.a(name:) breaks its @constraint: Too
|
|
69
|
+
small: expected string to have >=2 characters`. It works for arguments and
|
|
70
|
+
input fields (`The default value of Page.size breaks ...`).
|
|
71
|
+
- It builds every schema once, at the call, so a constraint that cannot apply
|
|
72
|
+
throws then, with the place named, not on the first request. This includes
|
|
73
|
+
constraints in input types no argument reaches.
|
|
74
|
+
- A subscription field is checked once, in `subscribe`, before it starts, not
|
|
75
|
+
on every event.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import type { GraphQLSchema } from 'graphql';
|
|
79
|
+
|
|
80
|
+
declare function withValidation<S extends GraphQLSchema>(schema: S): S;
|
|
81
|
+
declare const constraintTypeDefs: string;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The directives are read from the SDL. A **code-first** schema does not declare
|
|
85
|
+
`@constraint`, so `withValidation` throws `this schema declares no @constraint
|
|
86
|
+
directive`. A schema that declares it but whose constrained types and fields
|
|
87
|
+
have no SDL (built with `new GraphQLObjectType`, or from merged pieces) is not
|
|
88
|
+
checked, and nothing says so.
|
|
89
|
+
|
|
90
|
+
## The `@constraint` arguments
|
|
91
|
+
|
|
92
|
+
They are those of [graphql-constraint-directive](https://github.com/confuser/graphql-constraint-directive),
|
|
93
|
+
minus `uniqueTypeName`. A rule narrows the type it names; on any other type the
|
|
94
|
+
schema fails to build (see [Troubleshooting](../troubleshooting.md)).
|
|
95
|
+
|
|
96
|
+
| Argument | GraphQL type | Applies to | Zod equivalent |
|
|
97
|
+
| --- | --- | --- | --- |
|
|
98
|
+
| `format` | `String` | `String`, `ID` | the schema of the [format](#formats), applied first |
|
|
99
|
+
| `minLength` | `Int` | `String`, `ID` | `.min(n)` |
|
|
100
|
+
| `maxLength` | `Int` | `String`, `ID` | `.max(n)` |
|
|
101
|
+
| `startsWith` | `String` | `String`, `ID` | `.startsWith(s)` |
|
|
102
|
+
| `endsWith` | `String` | `String`, `ID` | `.endsWith(s)` |
|
|
103
|
+
| `contains` | `String` | `String`, `ID` | `.includes(s)` |
|
|
104
|
+
| `notContains` | `String` | `String`, `ID` | `.refine(...)`, message `Must not contain "s"` |
|
|
105
|
+
| `pattern` | `String` | `String`, `ID` | `.regex(new RegExp(p))` |
|
|
106
|
+
| `min` | `Float` | `Int`, `Float` | `.gte(n)` |
|
|
107
|
+
| `max` | `Float` | `Int`, `Float` | `.lte(n)` |
|
|
108
|
+
| `exclusiveMin` | `Float` | `Int`, `Float` | `.gt(n)` |
|
|
109
|
+
| `exclusiveMax` | `Float` | `Int`, `Float` | `.lt(n)` |
|
|
110
|
+
| `multipleOf` | `Float` | `Int`, `Float` | `.multipleOf(n)` |
|
|
111
|
+
| `minItems` | `Int` | a list | `.min(n)` on the array |
|
|
112
|
+
| `maxItems` | `Int` | a list | `.max(n)` on the array |
|
|
113
|
+
|
|
114
|
+
An `Int` is also checked to be an integer. Several arguments combine:
|
|
115
|
+
|
|
116
|
+
```graphql
|
|
117
|
+
input Product {
|
|
118
|
+
sku: String! @constraint(startsWith: "SKU-", maxLength: 12, pattern: "^SKU-[0-9]+$")
|
|
119
|
+
price: Float! @constraint(exclusiveMin: 0, multipleOf: 0.01)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The directive must be the one `constraintTypeDefs` declares. A `@constraint`
|
|
124
|
+
declared otherwise, typically graphql-constraint-directive's SDL kept after a
|
|
125
|
+
migration, makes `withValidation` throw at startup: `This schema's @constraint
|
|
126
|
+
is not constraintTypeDefs': it is allowed on FIELD_DEFINITION; declares
|
|
127
|
+
uniqueTypeName, which no rule reads. Declare it with constraintTypeDefs.` An
|
|
128
|
+
argument of another type is named too (`declares minLength: String, not Int`).
|
|
129
|
+
See [Troubleshooting](../troubleshooting.md).
|
|
130
|
+
|
|
131
|
+
`@constraint` is allowed on `ARGUMENT_DEFINITION` and `INPUT_FIELD_DEFINITION`
|
|
132
|
+
only. On an output field `buildSchema` fails with
|
|
133
|
+
`Directive "@constraint" may not be used on FIELD_DEFINITION.`, where
|
|
134
|
+
graphql-constraint-directive allows it: a resolver's result is the job of
|
|
135
|
+
output scalars, and a directive that checked nothing there would be a promise
|
|
136
|
+
broken in silence. Use [`@nxgt/graphql-scalars`](https://www.npmjs.com/package/@nxgt/graphql-scalars)
|
|
137
|
+
for that.
|
|
138
|
+
|
|
139
|
+
### Lists
|
|
140
|
+
|
|
141
|
+
`minItems` and `maxItems` apply to the list; every other rule applies to its
|
|
142
|
+
items. A nullable argument also accepts `null` and absence.
|
|
143
|
+
|
|
144
|
+
```graphql
|
|
145
|
+
type Query {
|
|
146
|
+
# at least one tag, at most 5; each tag 1 to 20 characters
|
|
147
|
+
search(tags: [String!]! @constraint(minItems: 1, maxItems: 5, minLength: 1, maxLength: 20)): [String!]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Nested and recursive inputs
|
|
152
|
+
|
|
153
|
+
Constraints on input fields are found through any depth of input types, lists
|
|
154
|
+
of them, and recursive ones. The issue path says where:
|
|
155
|
+
|
|
156
|
+
```graphql
|
|
157
|
+
input Address { zip: String! @constraint(pattern: "^[0-9]{5}$") }
|
|
158
|
+
input SignUp { name: String, address: Address! }
|
|
159
|
+
type Mutation { signUp(input: SignUp!): Boolean }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
A bad `zip` is reported at `['input', 'address', 'zip']`.
|
|
163
|
+
|
|
164
|
+
### Interfaces
|
|
165
|
+
|
|
166
|
+
An interface field is resolved by the objects that implement it, so a
|
|
167
|
+
`@constraint` on an interface field's argument must be written again, the same,
|
|
168
|
+
on each implementing object's field. If an object drops it or writes a
|
|
169
|
+
different one, `withValidation` throws at startup rather than let the argument
|
|
170
|
+
go unchecked, printing both.
|
|
171
|
+
|
|
172
|
+
```graphql
|
|
173
|
+
interface Named { name(style: String @constraint(maxLength: 5)): String }
|
|
174
|
+
type Person implements Named {
|
|
175
|
+
name(style: String @constraint(maxLength: 5)): String
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Format first
|
|
180
|
+
|
|
181
|
+
`format` replaces the base string schema, and every other rule narrows it, so
|
|
182
|
+
the order you write them in does not matter:
|
|
183
|
+
|
|
184
|
+
```graphql
|
|
185
|
+
email: String! @constraint(maxLength: 12, format: "email")
|
|
186
|
+
# the email format, then at most 12 characters
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Formats
|
|
190
|
+
|
|
191
|
+
`@constraint(format: "...")` takes one of these names. Anything else fails at
|
|
192
|
+
startup with `Unknown @constraint format "siret". Known formats: byte, date,
|
|
193
|
+
date-time, email, ipv4, ipv6, uri, uuid.`
|
|
194
|
+
|
|
195
|
+
| Format | Zod schema | Accepts |
|
|
196
|
+
| --- | --- | --- |
|
|
197
|
+
| `byte` | `z.base64()` | base64 text |
|
|
198
|
+
| `date-time` | `z.iso.datetime({ offset: true })` | RFC 3339 with an offset: `2024-03-10T12:00:00Z`, `2024-03-10T12:00:00+02:00` |
|
|
199
|
+
| `date` | `z.iso.date()` | `YYYY-MM-DD`, a real day |
|
|
200
|
+
| `email` | `z.email()` | an email address |
|
|
201
|
+
| `ipv4` | `z.ipv4()` | a dotted IPv4 address |
|
|
202
|
+
| `ipv6` | `z.ipv6()` | an IPv6 address |
|
|
203
|
+
| `uri` | `z.url({ protocol: /^(https?\|ftp)$/ })` | an absolute `http`, `https` or `ftp` URL; the scheme is required |
|
|
204
|
+
| `uuid` | `z.uuid()` | an 8-4-4-4-12 UUID |
|
|
205
|
+
|
|
206
|
+
Where they differ from graphql-constraint-directive's (validator.js):
|
|
207
|
+
|
|
208
|
+
- `date-time` is the canonical form only: an uppercase `T` and `Z`, seconds
|
|
209
|
+
present. A lowercase `t` or `z`, or a space for `T`, is refused.
|
|
210
|
+
- `uri` requires the scheme (`example.com` is refused) and nothing but `http`,
|
|
211
|
+
`https` and `ftp` gets in: no `javascript:`, no `mailto:`.
|
|
212
|
+
|
|
213
|
+
## In your IDE
|
|
214
|
+
|
|
215
|
+
An IDE's GraphQL plugin does not run your server, so it does not know
|
|
216
|
+
`@constraint` and reports `Unknown directive "@constraint"`. Point
|
|
217
|
+
`graphql.config.yml` at the file the package ships (by path; it is not an
|
|
218
|
+
`exports` subpath, so do not `import` it):
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
schema:
|
|
222
|
+
- src/**/*.graphql
|
|
223
|
+
- node_modules/@nxgt/graphql-validation/graphql/constraint.graphqls
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
To keep a copy in the project, for an IDE that does not index `node_modules` or
|
|
227
|
+
to commit it with your schema:
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
bunx nxgt-graphql-validation typedefs --out
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
and list `generated/graphql/constraint.graphqls` in `graphql.config.yml` in place of the
|
|
234
|
+
`node_modules/...` line.
|
|
235
|
+
|
|
236
|
+
`npx` works the same. `--out` alone writes `generated/graphql/constraint.graphqls`,
|
|
237
|
+
`--out <file>` writes `<file>`, creating the folder, and without it the SDL goes
|
|
238
|
+
to stdout (`--help` or `-h`, anywhere on the line, lists the flags). It exits 0 when done, 1 when the file
|
|
239
|
+
cannot be written (`typedefs failed: <message>` on stderr), and 2 on a usage
|
|
240
|
+
error (`Unknown command "<x>".`, `Unexpected arguments: …`, or `No command.`). Regenerate the file after upgrading the
|
|
241
|
+
package.
|
|
242
|
+
|
|
243
|
+
The schema must declare `@constraint` once. Pick one source for the server:
|
|
244
|
+
|
|
245
|
+
- **The generated file is part of your schema.** If the server loads its
|
|
246
|
+
type definitions by scanning `*.graphql(s)` files and the copy sits among
|
|
247
|
+
them, it already declares the directive: do not add `constraintTypeDefs`.
|
|
248
|
+
- **`constraintTypeDefs` declares it.** Then the copy is for the IDE only:
|
|
249
|
+
keep it out of the folders the server scans (or exclude it from the glob).
|
|
250
|
+
|
|
251
|
+
Both give the same schema, and `withValidation` accepts either: it checks that
|
|
252
|
+
the directive declared is this package's. Both at once throw
|
|
253
|
+
`There can be only one directive named "@constraint".`
|
|
254
|
+
|
|
255
|
+
## A realistic schema
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { createYoga, createSchema } from 'graphql-yoga';
|
|
259
|
+
import { constraintTypeDefs, withValidation } from '@nxgt/graphql-validation';
|
|
260
|
+
|
|
261
|
+
const schema = withValidation(
|
|
262
|
+
createSchema({
|
|
263
|
+
typeDefs: [
|
|
264
|
+
constraintTypeDefs,
|
|
265
|
+
/* GraphQL */ `
|
|
266
|
+
input Signup {
|
|
267
|
+
email: String! @constraint(format: "email", maxLength: 254)
|
|
268
|
+
password: String! @constraint(minLength: 12)
|
|
269
|
+
website: String @constraint(format: "uri")
|
|
270
|
+
}
|
|
271
|
+
type Mutation {
|
|
272
|
+
signup(input: Signup!): ID!
|
|
273
|
+
}
|
|
274
|
+
type Query { ok: Boolean }
|
|
275
|
+
`,
|
|
276
|
+
],
|
|
277
|
+
resolvers: {
|
|
278
|
+
Mutation: {
|
|
279
|
+
// runs only for a valid `input`
|
|
280
|
+
signup: (_, { input }) => createUser(input),
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
declare function createUser(input: { email: string }): string;
|
|
287
|
+
|
|
288
|
+
export const yoga = createYoga({ schema });
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
For rules the directive cannot say, see [`validated`](errors.md#validated).
|