@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 @@
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,2 @@
1
+ export declare const maxItemsRule: import("./rule").Rule<"maxItems", "Int", "list">;
2
+ //# sourceMappingURL=max-items.d.ts.map
@@ -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,2 @@
1
+ export declare const maxLengthRule: import("./rule").Rule<"maxLength", "Int", "string">;
2
+ //# sourceMappingURL=max-length.d.ts.map
@@ -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,2 @@
1
+ export declare const maxRule: import("./rule").Rule<"max", "Float", "number">;
2
+ //# sourceMappingURL=max.d.ts.map
@@ -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,2 @@
1
+ export declare const minItemsRule: import("./rule").Rule<"minItems", "Int", "list">;
2
+ //# sourceMappingURL=min-items.d.ts.map
@@ -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,2 @@
1
+ export declare const minLengthRule: import("./rule").Rule<"minLength", "Int", "string">;
2
+ //# sourceMappingURL=min-length.d.ts.map
@@ -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,2 @@
1
+ export declare const minRule: import("./rule").Rule<"min", "Float", "number">;
2
+ //# sourceMappingURL=min.d.ts.map
@@ -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,2 @@
1
+ export declare const multipleOfRule: import("./rule").Rule<"multipleOf", "Float", "number">;
2
+ //# sourceMappingURL=multiple-of.d.ts.map
@@ -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,2 @@
1
+ export declare const notContainsRule: import("./rule").Rule<"notContains", "String", "string">;
2
+ //# sourceMappingURL=not-contains.d.ts.map
@@ -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,2 @@
1
+ export declare const patternRule: import("./rule").Rule<"pattern", "String", "string">;
2
+ //# sourceMappingURL=pattern.d.ts.map
@@ -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,2 @@
1
+ export declare const startsWithRule: import("./rule").Rule<"startsWith", "String", "string">;
2
+ //# sourceMappingURL=starts-with.d.ts.map
@@ -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).