Skip to content

Commit 1f2efa4

Browse files
committed
feat(validation): make class-validator truly optional dependency
1 parent ba7f056 commit 1f2efa4

File tree

14 files changed

+113
-32
lines changed

14 files changed

+113
-32
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- **Breaking Change**: `AuthChecker` type is now "function or class" - update to `AuthCheckerFn` if the function form is needed in the code
77
- **Breaking Change**: update `graphql-js` peer dependency to `^16.6.0`
88
- **Breaking Change**: `buildSchemaSync` is now also checking the generated schema for errors
9+
- **Breaking Change**: `validate` option of `buildSchema` is set to `false` by default - integration with `class-validator` has to be turned on explicitly
10+
- **Breaking Change**: `validate` option of `buildSchema` doesn't accept anymore a custom validation function - use `validateFn` option instead
911
- support class-based auth checker, which allows for dependency injection
1012
- allow defining directives for interface types and theirs fields, with inheritance for object types fields (#744)
1113
- allow deprecating input fields and args (#794)

docs/installation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ Before getting started with TypeGraphQL we need to install some additional depen
1010
1111
## Packages installation
1212

13-
First, we have to install the main package, as well as [`graphql-js`](https://github.com/graphql/graphql-js) and [`class-validator`](https://github.com/typestack/class-validator) which are peer dependencies of TypeGraphQL:
13+
First, we have to install the main package, as well as [`graphql-js`](https://github.com/graphql/graphql-js) which is a peer dependency of TypeGraphQL:
1414

1515
```sh
16-
npm i graphql class-validator type-graphql
16+
npm i graphql type-graphql
1717
```
1818

1919
Also, the `reflect-metadata` shim is required to make the type reflection work:

docs/validation.md

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ We can also use other libraries or our own custom solution, as described in [cus
1616

1717
### How to use
1818

19-
First we decorate the input/arguments class with the appropriate decorators from `class-validator`.
19+
First, we need to install the `class-validator` package:
20+
21+
```sh
22+
npm i class-validator
23+
```
24+
25+
Then we decorate the input/arguments class with the appropriate decorators from `class-validator`.
2026
So we take this:
2127

2228
```typescript
@@ -47,6 +53,15 @@ export class RecipeInput {
4753
}
4854
```
4955

56+
Then we need to enable the auto-validate feature (as it's disabled by default) by simply setting `validate: true` in `buildSchema` options, e.g.:
57+
58+
```typescript
59+
const schema = await buildSchema({
60+
resolvers: [RecipeResolver],
61+
validate: true, // enable `class-validator` integration
62+
});
63+
```
64+
5065
And that's it! 😉
5166

5267
TypeGraphQL will automatically validate our inputs and arguments based on the definitions:
@@ -66,16 +81,8 @@ export class RecipeResolver {
6681

6782
Of course, [there are many more decorators](https://github.com/typestack/class-validator#validation-decorators) we have access to, not just the simple `@Length` decorator used in the example above, so take a look at the `class-validator` documentation.
6883

69-
This feature is enabled by default. However, we can disable it if we must:
70-
71-
```typescript
72-
const schema = await buildSchema({
73-
resolvers: [RecipeResolver],
74-
validate: false, // disable automatic validation or pass the default config object
75-
});
76-
```
77-
78-
And we can still enable it per resolver's argument if we need to:
84+
We don't have to enable this feature for all resolvers.
85+
It's also possible to keep `validate: false` in `buildSchema` options but still enable it per resolver's argument if we need to:
7986

8087
```typescript
8188
class RecipeResolver {
@@ -101,6 +108,7 @@ class RecipeResolver {
101108
```
102109

103110
Note that by default, the `skipMissingProperties` setting of the `class-validator` is set to `true` because GraphQL will independently check whether the params/fields exist or not.
111+
Same goes to `forbidUnknownValues` setting which is set to `false` because the GraphQL runtime checks for additional data, not described in schema.
104112

105113
GraphQL will also check whether the fields have correct types (String, Int, Float, Boolean, etc.) so we don't have to use the `@IsOptional`, `@Allow`, `@IsString` or the `@IsInt` decorators at all!
106114

@@ -193,15 +201,15 @@ It receives two parameters:
193201
- `argValue` which is the injected value of `@Arg()` or `@Args()`
194202
- `argType` which is a runtime type information (e.g. `String` or `RecipeInput`).
195203

196-
The `validate` function can be async and should return nothing (`void`) when validation passes or throw an error when validation fails.
197-
So be aware of this while trying to wrap another library in `validate` function for TypeGraphQL.
204+
The `validateFn` option can be an async function and should return nothing (`void`) when validation passes or throw an error when validation fails.
205+
So be aware of this while trying to wrap another library in `validateFn` function for TypeGraphQL.
198206

199207
Example using [decorators library for Joi validators (`joiful`)](https://github.com/joiful-ts/joiful):
200208

201209
```ts
202210
const schema = await buildSchema({
203211
// ...other options
204-
validate: argValue => {
212+
validateFn: argValue => {
205213
// call joiful validate
206214
const { error } = joiful.validate(argValue);
207215
if (error) {

examples/automatic-validation/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ async function bootstrap() {
1010
const schema = await buildSchema({
1111
resolvers: [RecipeResolver],
1212
emitSchemaFile: path.resolve(__dirname, "schema.gql"),
13+
// remember to turn on validation!
14+
validate: true,
1315
});
1416

1517
// Create GraphQL server

examples/custom-validation/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ async function bootstrap() {
1212
resolvers: [RecipeResolver],
1313
emitSchemaFile: path.resolve(__dirname, "schema.gql"),
1414
// custom validate function
15-
validate: (argValue, argType) => {
15+
validateFn: (argValue, argType) => {
1616
// call joiful validate
1717
const { error } = joiful.validate(argValue);
1818
if (error) {

examples/mixin-classes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ async function bootstrap() {
1111
resolvers: [UserResolver],
1212
emitSchemaFile: path.resolve(__dirname, "schema.gql"),
1313
skipCheck: true,
14+
validate: true,
1415
});
1516

1617
// Create GraphQL server

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@
2121
"postgenerate:sponsorkit": "npx shx cp ./img/github-sponsors.svg ./website/static/img/github-sponsors.svg"
2222
},
2323
"peerDependencies": {
24-
"class-validator": ">=0.14.0",
25-
"graphql": "^16.6.0"
24+
"graphql": "^16.6.0",
25+
"class-validator": ">=0.14.0"
26+
},
27+
"peerDependenciesMeta": {
28+
"class-validator": {
29+
"optional": true
30+
}
2631
},
2732
"dependencies": {
2833
"@types/node": "*",

src/errors/ArgumentValidationError.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @ts-ignore `class-validator` might not be installed by user
12
import type { ValidationError } from "class-validator";
23

34
export class ArgumentValidationError extends Error {

src/resolvers/create.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function createHandlerResolver(
1818
): GraphQLFieldResolver<any, any, any> {
1919
const {
2020
validate: globalValidate,
21+
validateFn,
2122
authChecker,
2223
authMode,
2324
pubSub,
@@ -40,6 +41,7 @@ export function createHandlerResolver(
4041
resolverMetadata.params!,
4142
resolverData,
4243
globalValidate,
44+
validateFn,
4345
pubSub,
4446
);
4547
if (isPromiseLike(params)) {
@@ -57,6 +59,7 @@ export function createHandlerResolver(
5759
resolverMetadata.params!,
5860
resolverData,
5961
globalValidate,
62+
validateFn,
6063
pubSub,
6164
);
6265
const targetInstance = targetInstanceOrPromise;
@@ -81,6 +84,7 @@ export function createAdvancedFieldResolver(
8184
const targetType = fieldResolverMetadata.getObjectType!();
8285
const {
8386
validate: globalValidate,
87+
validateFn: validateFn,
8488
authChecker,
8589
authMode,
8690
pubSub,
@@ -104,6 +108,7 @@ export function createAdvancedFieldResolver(
104108
fieldResolverMetadata.params!,
105109
resolverData,
106110
globalValidate,
111+
validateFn,
107112
pubSub,
108113
);
109114
if (isPromiseLike(params)) {

src/resolvers/helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import { AuthMiddleware } from "../helpers/auth-middleware";
1010
import { convertArgsToInstance, convertArgToInstance } from "./convert-args";
1111
import isPromiseLike from "../utils/isPromiseLike";
1212
import { ValidateSettings } from "../schema/build-context";
13+
import { ValidatorFn } from "../interfaces/ValidatorFn";
1314

1415
export function getParams(
1516
params: ParamMetadata[],
1617
resolverData: ResolverData<any>,
1718
globalValidate: ValidateSettings,
19+
validateFn: ValidatorFn<object> | undefined,
1820
pubSub: PubSubEngine,
1921
): Promise<any[]> | any[] {
2022
const paramValues = params
@@ -27,13 +29,15 @@ export function getParams(
2729
paramInfo.getType(),
2830
globalValidate,
2931
paramInfo.validate,
32+
validateFn,
3033
);
3134
case "arg":
3235
return validateArg(
3336
convertArgToInstance(paramInfo, resolverData.args),
3437
paramInfo.getType(),
3538
globalValidate,
3639
paramInfo.validate,
40+
validateFn,
3741
);
3842
case "context":
3943
if (paramInfo.propertyName) {

src/resolvers/validate-arg.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
// @ts-ignore `class-validator` might not be installed by user
12
import type { ValidatorOptions } from "class-validator";
2-
import { TypeValue } from "../decorators/types";
33

4+
import { TypeValue } from "../decorators/types";
45
import { ArgumentValidationError } from "../errors/ArgumentValidationError";
56
import { ValidateSettings } from "../schema/build-context";
7+
import { ValidatorFn } from "../interfaces/ValidatorFn";
68

79
const shouldArgBeValidated = (argValue: unknown): boolean =>
810
argValue !== null && typeof argValue === "object";
@@ -12,14 +14,15 @@ export async function validateArg<T extends object>(
1214
argType: TypeValue,
1315
globalValidate: ValidateSettings,
1416
argValidate: ValidateSettings | undefined,
17+
validateFn: ValidatorFn<object> | undefined,
1518
): Promise<T | undefined> {
16-
const validate = argValidate !== undefined ? argValidate : globalValidate;
17-
if (validate === false || !shouldArgBeValidated(argValue)) {
19+
if (typeof validateFn === "function") {
20+
await validateFn(argValue, argType);
1821
return argValue;
1922
}
2023

21-
if (typeof validate === "function") {
22-
await validate(argValue, argType);
24+
const validate = argValidate !== undefined ? argValidate : globalValidate;
25+
if (validate === false || !shouldArgBeValidated(argValue)) {
2326
return argValue;
2427
}
2528

@@ -35,6 +38,7 @@ export async function validateArg<T extends object>(
3538
validatorOptions.forbidUnknownValues = false;
3639
}
3740

41+
// dynamic import to avoid making `class-validator` a peer dependency when `validate: true` is not set
3842
const { validateOrReject } = await import("class-validator");
3943
try {
4044
if (Array.isArray(argValue)) {

src/schema/build-context.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { GraphQLScalarType } from "graphql";
2+
// @ts-ignore `class-validator` might not be installed by user
23
import type { ValidatorOptions } from "class-validator";
34
import { PubSubEngine, PubSub, PubSubOptions } from "graphql-subscriptions";
45

@@ -14,17 +15,20 @@ export interface ScalarsTypeMap {
1415
scalar: GraphQLScalarType;
1516
}
1617

17-
export type ValidateSettings = boolean | ValidatorOptions | ValidatorFn<object>;
18+
export type ValidateSettings = boolean | ValidatorOptions;
1819

1920
export interface BuildContextOptions {
2021
dateScalarMode?: DateScalarMode;
2122
scalarsMap?: ScalarsTypeMap[];
2223
/**
2324
* Indicates if class-validator should be used to auto validate objects injected into params.
2425
* You can directly pass validator options to enable validator with a given options.
25-
* Also, you can provide your own validation function to check the args.
2626
*/
2727
validate?: ValidateSettings;
28+
/**
29+
* Own validation function to check the args and inputs.
30+
*/
31+
validateFn?: ValidatorFn<object>;
2832
authChecker?: AuthChecker<any, any>;
2933
authMode?: AuthMode;
3034
pubSub?: PubSubEngine | PubSubOptions;
@@ -44,6 +48,7 @@ export abstract class BuildContext {
4448
static dateScalarMode: DateScalarMode;
4549
static scalarsMaps: ScalarsTypeMap[];
4650
static validate: ValidateSettings;
51+
static validateFn?: ValidatorFn<object>;
4752
static authChecker?: AuthChecker<any, any>;
4853
static authMode: AuthMode;
4954
static pubSub: PubSubEngine;
@@ -68,6 +73,10 @@ export abstract class BuildContext {
6873
this.validate = options.validate;
6974
}
7075

76+
if (options.validateFn !== undefined) {
77+
this.validateFn = options.validateFn;
78+
}
79+
7180
if (options.authChecker !== undefined) {
7281
this.authChecker = options.authChecker;
7382
}
@@ -105,7 +114,8 @@ export abstract class BuildContext {
105114
static reset() {
106115
this.dateScalarMode = "isoDate";
107116
this.scalarsMaps = [];
108-
this.validate = true;
117+
this.validate = false;
118+
this.validateFn = undefined;
109119
this.authChecker = undefined;
110120
this.authMode = "error";
111121
this.pubSub = new PubSub();

tests/functional/typedefs-resolvers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ describe("typeDefs and resolvers", () => {
243243
pubSub,
244244
container: Container,
245245
orphanedTypes: [SampleType1],
246+
validate: true,
246247
}));
247248
schema = makeExecutableSchema({
248249
typeDefs,

tests/functional/validation.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,45 @@ describe("Validation", () => {
382382
localArgsData = undefined;
383383
});
384384

385-
it("should pass incorrect args when validation is turned off", async () => {
385+
it("should pass incorrect args when validation is turned off by default", async () => {
386+
getMetadataStorage().clear();
387+
388+
@ObjectType()
389+
class SampleObject {
390+
@Field({ nullable: true })
391+
field?: string;
392+
}
393+
@ArgsType()
394+
class SampleArguments {
395+
@Field()
396+
@MaxLength(5)
397+
field: string;
398+
}
399+
@Resolver(of => SampleObject)
400+
class SampleResolver {
401+
@Query()
402+
sampleQuery(@Args() args: SampleArguments): SampleObject {
403+
localArgsData = args;
404+
return {};
405+
}
406+
}
407+
const localSchema = await buildSchema({
408+
resolvers: [SampleResolver],
409+
// default - `validate: false,`
410+
});
411+
412+
const query = `query {
413+
sampleQuery(
414+
field: "12345678",
415+
) {
416+
field
417+
}
418+
}`;
419+
await graphql({ schema: localSchema, source: query });
420+
expect(localArgsData).toEqual({ field: "12345678" });
421+
});
422+
423+
it("should pass incorrect args when validation is turned off explicitly", async () => {
386424
getMetadataStorage().clear();
387425

388426
@ObjectType()
@@ -667,10 +705,10 @@ describe("Custom validation", () => {
667705
sampleQueryArgs = [];
668706
});
669707

670-
it("should call `validate` function provided in option with proper params", async () => {
708+
it("should call `validateFn` function provided in option with proper params", async () => {
671709
schema = await buildSchema({
672710
resolvers: [sampleResolverCls],
673-
validate: (arg, type) => {
711+
validateFn: (arg, type) => {
674712
validateArgs.push(arg);
675713
validateTypes.push(type);
676714
},
@@ -686,7 +724,7 @@ describe("Custom validation", () => {
686724
it("should inject validated arg as resolver param", async () => {
687725
schema = await buildSchema({
688726
resolvers: [sampleResolverCls],
689-
validate: () => {
727+
validateFn: () => {
690728
// do nothing
691729
},
692730
});
@@ -699,7 +737,7 @@ describe("Custom validation", () => {
699737
it("should rethrow wrapped error when error thrown in `validate`", async () => {
700738
schema = await buildSchema({
701739
resolvers: [sampleResolverCls],
702-
validate: () => {
740+
validateFn: () => {
703741
throw new Error("Test validate error");
704742
},
705743
});

0 commit comments

Comments
 (0)