Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- add basic support for directives with `@Directive()` decorator (#369)
### Fixes
- refactor union types function syntax handling to prevent possible errors with circular refs
- fix transforming and validating nested inputs and arrays (#462)

## v0.17.5
### Features
Expand Down
2 changes: 2 additions & 0 deletions docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ Note that by default, the `skipMissingProperties` setting of the `class-validato

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!

However, when using nested input or arrays, we always have to use [`@ValidateNested()` decorator](https://github.com/typestack/class-validator#validating-nested-objects) or [`{ each: true }` option](https://github.com/typestack/class-validator#validating-arrays) to make nested validation work properly.

## Response to the Client

When a client sends incorrect data to the server:
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ export function convertToType(Target: any, data?: object): object | undefined {
if (data instanceof Target) {
return data;
}
// convert array to instances
if (Array.isArray(data)) {
return data.map(item => convertToType(Target, item));
}

return Object.assign(new Target(), data);
}
Expand Down
138 changes: 138 additions & 0 deletions src/resolvers/convert-args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { ArgParamMetadata, ClassMetadata, ArgsParamMetadata } from "../metadata/definitions";
import { convertToType } from "../helpers/types";
import { ArgsDictionary, ClassType } from "../interfaces";
import { getMetadataStorage } from "../metadata/getMetadataStorage";
import { TypeValue } from "../decorators/types";

interface TransformationTreeField {
name: string;
target: TypeValue;
fields?: TransformationTree;
}

interface TransformationTree {
target: TypeValue;
getFields: () => TransformationTreeField[];
}

const generatedTrees = new Map<TypeValue, TransformationTree | null>();

function getInputType(target: TypeValue): ClassMetadata | undefined {
return getMetadataStorage().inputTypes.find(t => t.target === target);
}

function getArgsType(target: TypeValue): ClassMetadata | undefined {
return getMetadataStorage().argumentTypes.find(t => t.target === target);
}

function generateInstanceTransformationTree(target: TypeValue): TransformationTree | null {
if (generatedTrees.has(target)) {
return generatedTrees.get(target)!;
}

const inputType = getInputType(target);
if (!inputType) {
generatedTrees.set(target, null);
return null;
}

function generateTransformationTree(metadata: ClassMetadata): TransformationTree {
let inputFields = metadata.fields!;
let superClass = Object.getPrototypeOf(metadata.target);
while (superClass.prototype !== undefined) {
const superInputType = getInputType(superClass);
if (superInputType) {
inputFields = [...inputFields, ...superInputType.fields!];
}
superClass = Object.getPrototypeOf(superClass);
}

const transformationTree: TransformationTree = {
target: metadata.target,
getFields: () =>
inputFields.map<TransformationTreeField>(field => {
const fieldTarget = field.getType();
const fieldInputType = getInputType(fieldTarget);
return {
name: field.name,
target: fieldTarget,
fields:
fieldTarget === metadata.target
? transformationTree
: fieldInputType && generateTransformationTree(fieldInputType),
};
}),
};

return transformationTree;
}

const generatedTransformationTree = generateTransformationTree(inputType);
generatedTrees.set(target, generatedTransformationTree);
return generatedTransformationTree;
}

function convertToInput(tree: TransformationTree, data: any) {
const inputFields = tree.getFields().reduce<Record<string, any>>((fields, field) => {
const siblings = field.fields;
const value = data[field.name];
if (!siblings || !value) {
fields[field.name] = convertToType(field.target, value);
} else if (Array.isArray(value)) {
fields[field.name] = value.map(itemValue => convertToInput(siblings, itemValue));
} else {
fields[field.name] = convertToInput(siblings, value);
}
return fields;
}, {});

return convertToType(tree.target, inputFields);
}

function convertValueToInstance(target: TypeValue, value: any) {
const transformationTree = generateInstanceTransformationTree(target);
return transformationTree
? convertToInput(transformationTree, value)
: convertToType(target, value);
}

function convertValuesToInstances(target: TypeValue, value: any) {
// skip converting undefined and null
if (value == null) {
return value;
}
if (Array.isArray(value)) {
return value.map(itemValue => convertValueToInstance(target, itemValue));
}
return convertValueToInstance(target, value);
}

export function convertArgsToInstance(argsMetadata: ArgsParamMetadata, args: ArgsDictionary) {
const ArgsClass = argsMetadata.getType() as ClassType;
const argsType = getArgsType(ArgsClass)!;

let argsFields = argsType.fields!;
let superClass = Object.getPrototypeOf(argsType.target);
while (superClass.prototype !== undefined) {
const superArgumentType = getArgsType(superClass);
if (superArgumentType) {
argsFields = [...argsFields, ...superArgumentType.fields!];
}
superClass = Object.getPrototypeOf(superClass);
}

const transformedFields = argsFields.reduce<Record<string, any>>((fields, field) => {
const fieldValue = args[field.name];
const fieldTarget = field.getType();
fields[field.name] = convertValuesToInstances(fieldTarget, fieldValue);
return fields;
}, {});

return convertToType(ArgsClass, transformedFields);
}

export function convertArgToInstance(argMetadata: ArgParamMetadata, args: ArgsDictionary) {
const argValue = args[argMetadata.name];
const argTarget = argMetadata.getType();
return convertValuesToInstances(argTarget, argValue);
}
5 changes: 3 additions & 2 deletions src/resolvers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ResolverData, AuthChecker, AuthMode } from "../interfaces";
import { Middleware, MiddlewareFn, MiddlewareClass } from "../interfaces/Middleware";
import { IOCContainer } from "../utils/container";
import { AuthMiddleware } from "../helpers/auth-middleware";
import { convertArgsToInstance, convertArgToInstance } from "./convert-args";

export async function getParams(
params: ParamMetadata[],
Expand All @@ -22,13 +23,13 @@ export async function getParams(
switch (paramInfo.kind) {
case "args":
return await validateArg(
convertToType(paramInfo.getType(), resolverData.args),
convertArgsToInstance(paramInfo, resolverData.args),
globalValidate,
paramInfo.validate,
);
case "arg":
return await validateArg(
convertToType(paramInfo.getType(), resolverData.args[paramInfo.name]),
convertArgToInstance(paramInfo, resolverData.args),
globalValidate,
paramInfo.validate,
);
Expand Down
6 changes: 5 additions & 1 deletion src/resolvers/validate-arg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ export async function validateArg<T extends Object>(

const { validateOrReject } = await import("class-validator");
try {
await validateOrReject(arg, validatorOptions);
if (Array.isArray(arg)) {
await Promise.all(arg.map(argItem => validateOrReject(argItem, validatorOptions)));
} else {
await validateOrReject(arg, validatorOptions);
}
return arg;
} catch (err) {
throw new ArgumentValidationError(err);
Expand Down
2 changes: 1 addition & 1 deletion src/schema/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ export abstract class SchemaGenerator {
while (superClass.prototype !== undefined) {
const superArgumentType = getMetadataStorage().argumentTypes.find(
it => it.target === superClass,
)!;
);
if (superArgumentType) {
this.mapArgFields(superArgumentType, args);
}
Expand Down
Loading