Skip to content

Commit

Permalink
fix(inputs): transform and validate nested inputs and arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
MichalLytek committed Nov 10, 2019
1 parent 0d716c8 commit 32f82e3
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 50 deletions.
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
134 changes: 134 additions & 0 deletions src/resolvers/convert-args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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) {
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

0 comments on commit 32f82e3

Please sign in to comment.