Skip to content

Commit

Permalink
feat(validation): support custom validate fn on args decorator level
Browse files Browse the repository at this point in the history
  • Loading branch information
MichalLytek committed Sep 20, 2023
1 parent 582a9cb commit 3914b78
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 20 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- **Breaking Change**: expose shim as a package entry point `type-graphql/shim` (and `/node_modules/type-graphql/build/typings/shim.ts`)
- support defining directives on `@Field` of `@Args`
- support defining directives on inline `@Arg`
- allow passing custom validation function as `validateFn` option of `@Arg` and `@Args` decorators

## v2.0.0-beta.3

Expand Down
31 changes: 26 additions & 5 deletions docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,21 +203,23 @@ An alternative solution that allows to completely get rid off big `class-validat

We can also use other libraries than `class-validator` together with TypeGraphQL.

To integrate it, all we need to do is to provide a custom function as `validate` option in `buildSchema`.
It receives two parameters:
To integrate it, all we need to do is to provide a custom function.
It receives three parameters:

- `argValue` which is the injected value of `@Arg()` or `@Args()`
- `argType` which is a runtime type information (e.g. `String` or `RecipeInput`).
- `argType` which is a runtime type information (e.g. `String` or `RecipeInput`)
- `resolverData` which holds the resolver execution context, described as generic type `ResolverData<TContext>`

The `validateFn` option can be an async function and should return nothing (`void`) when validation passes or throw an error when validation fails.
This function can be an async function and should return nothing (`void`) when validation passes, or throw an error when validation fails.
So be aware of this while trying to wrap another library in `validateFn` function for TypeGraphQL.

Then we provide this function as a `validateFn` option in `buildSchema`.
Example using [decorators library for Joi validators (`joiful`)](https://github.com/joiful-ts/joiful):

```ts
const schema = await buildSchema({
// ...
validate: argValue => {
validateFn: argValue => {
// Call joiful validate
const { error } = joiful.validate(argValue);
if (error) {
Expand All @@ -228,6 +230,25 @@ const schema = await buildSchema({
});
```

The `validateFn` option is also supported as a `@Arg()` or `@Args()` decorator option, e.g.:

```ts
@Resolver()
class SampleResolver {
@Query()
sampleQuery(
@Arg("sampleArg", {
validateFn: (argValue, argType) => {
// Do something here with arg value and type...
},
})
sampleArg: string,
): string {
// ...
}
}
```

> Be aware that when using custom validator, the error won't be wrapped with `ArgumentValidationError` like for the built-in `class-validator` validation.
### Custom Validation Example
Expand Down
2 changes: 2 additions & 0 deletions src/decorators/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type ResolverFilterData,
type ResolverTopicData,
type TypeResolver,
type ValidatorFn,
} from "@/typings";

export type RecursiveArray<TValue> = Array<RecursiveArray<TValue> | TValue>;
Expand Down Expand Up @@ -45,6 +46,7 @@ export interface DeprecationOptions {
}
export interface ValidateOptions {
validate?: ValidateSettings;
validateFn?: ValidatorFn;
}
export interface ComplexityOptions {
complexity?: Complexity;
Expand Down
3 changes: 2 additions & 1 deletion src/helpers/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function getParamInfo({
index: parameterIndex,
getType,
typeOptions,
validate: options.validate,
validateSettings: options.validate,
validateFn: options.validateFn,
};
}
5 changes: 3 additions & 2 deletions src/metadata/definitions/param-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type TypeOptions, type TypeValueThunk } from "@/decorators/types";
import { type ValidateSettings } from "@/schema/build-context";
import { type ResolverData } from "@/typings";
import { type ResolverData, type ValidatorFn } from "@/typings";

export interface BasicParamMetadata {
target: Function;
Expand Down Expand Up @@ -31,7 +31,8 @@ export type RootParamMetadata = {
export type CommonArgMetadata = {
getType: TypeValueThunk;
typeOptions: TypeOptions;
validate: ValidateSettings | undefined;
validateSettings: ValidateSettings | undefined;
validateFn: ValidatorFn | undefined;
} & BasicParamMetadata;

export type ArgParamMetadata = {
Expand Down
12 changes: 7 additions & 5 deletions src/resolvers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function getParams(
params: ParamMetadata[],
resolverData: ResolverData<any>,
globalValidate: ValidateSettings,
validateFn: ValidatorFn | undefined,
globalValidateFn: ValidatorFn | undefined,
pubSub: PubSubEngine,
): Promise<any[]> | any[] {
const paramValues = params
Expand All @@ -28,8 +28,9 @@ export function getParams(
paramInfo.getType(),
resolverData,
globalValidate,
paramInfo.validate,
validateFn,
paramInfo.validateSettings,
globalValidateFn,
paramInfo.validateFn,
);

case "arg":
Expand All @@ -38,8 +39,9 @@ export function getParams(
paramInfo.getType(),
resolverData,
globalValidate,
paramInfo.validate,
validateFn,
paramInfo.validateSettings,
globalValidateFn,
paramInfo.validateFn,
);

case "context":
Expand Down
14 changes: 8 additions & 6 deletions src/resolvers/validate-arg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,25 @@ export async function validateArg(
argValue: any | undefined,
argType: TypeValue,
resolverData: ResolverData,
globalValidate: ValidateSettings,
argValidate: ValidateSettings | undefined,
validateFn: ValidatorFn | undefined,
globalValidateSettings: ValidateSettings,
argValidateSettings: ValidateSettings | undefined,
globalValidateFn: ValidatorFn | undefined,
argValidateFn: ValidatorFn | undefined,
): Promise<any | undefined> {
const validateFn = argValidateFn ?? globalValidateFn;
if (typeof validateFn === "function") {
await validateFn(argValue, argType, resolverData);
return argValue;
}

const validate = argValidate !== undefined ? argValidate : globalValidate;
const validate = argValidateSettings !== undefined ? argValidateSettings : globalValidateSettings;
if (validate === false || !shouldArgBeValidated(argValue)) {
return argValue;
}

const validatorOptions: ValidatorOptions = {
...(typeof globalValidate === "object" ? globalValidate : {}),
...(typeof argValidate === "object" ? argValidate : {}),
...(typeof globalValidateSettings === "object" ? globalValidateSettings : {}),
...(typeof argValidateSettings === "object" ? argValidateSettings : {}),
};
if (validatorOptions.skipMissingProperties !== false) {
validatorOptions.skipMissingProperties = true;
Expand Down
42 changes: 41 additions & 1 deletion tests/functional/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ describe("Custom validation", () => {

let validateArgs: Array<any | undefined> = [];
let validateTypes: TypeValue[] = [];
const validateResolverData: ResolverData[] = [];
let validateResolverData: ResolverData[] = [];
let sampleQueryArgs: any[] = [];

beforeAll(async () => {
Expand Down Expand Up @@ -707,13 +707,28 @@ describe("Custom validation", () => {
sampleQueryArgs.push(arrayArg);
return true;
}

@Query()
sampleInlineArgValidateFnQuery(
@Arg("arg", {
validateFn: (arg, type, resolverData) => {
validateArgs.push(arg);
validateTypes.push(type);
validateResolverData.push(resolverData);
},
})
arg: SampleInput,
): string {
return arg.sampleField;
}
}
sampleResolverCls = SampleResolver;
});

beforeEach(() => {
validateArgs = [];
validateTypes = [];
validateResolverData = [];
sampleQueryArgs = [];
});

Expand Down Expand Up @@ -775,6 +790,31 @@ describe("Custom validation", () => {
expect(sampleQueryArgs).toEqual([{ sampleField: "sampleFieldValue" }]);
});

it("should call `validateFn` function provided inline in arg option with proper params", async () => {
schema = await buildSchema({
resolvers: [sampleResolverCls],
});

await graphql({
schema,
source: /* graphql */ `
query {
sampleInlineArgValidateFnQuery(arg: { sampleField: "sampleArgValue" })
}
`,
contextValue: { isContext: true },
});

expect(validateArgs).toEqual([{ sampleField: "sampleArgValue" }]);
expect(validateArgs[0]).toBeInstanceOf(sampleInputCls);
expect(validateTypes).toEqual([sampleInputCls]);
expect(validateResolverData).toEqual([
expect.objectContaining({
context: { isContext: true },
}),
]);
});

it("should rethrow wrapped error when error thrown in `validate`", async () => {
schema = await buildSchema({
resolvers: [sampleResolverCls],
Expand Down

0 comments on commit 3914b78

Please sign in to comment.