diff --git a/src/resolvers/convert-arg.ts b/src/resolvers/convert-arg.ts new file mode 100644 index 000000000..2af9c80c7 --- /dev/null +++ b/src/resolvers/convert-arg.ts @@ -0,0 +1,99 @@ +import { ArgParamMetadata, ClassMetadata } from "../metadata/definitions"; +import { convertToType } from "../helpers/types"; +import { ArgsDictionary } from "../interfaces"; +import { ClassType } from "../interfaces/ClassType"; +import { getMetadataStorage } from "../metadata/getMetadataStorage"; + +interface InputTypeField { + name: string; + fields?: InputTypeClass; + isArray?: boolean; +} + +interface InputTypeClass { + target: ClassType; + fields: InputTypeField[]; +} + +const generatedTrees = new Map(); + +function getInputType(target: ClassType): ClassMetadata | undefined { + return getMetadataStorage().inputTypes.find(t => t.target === target); +} + +function generateTree(param: ArgParamMetadata): InputTypeClass | null { + const target = param.getType() as ClassType; + + if (generatedTrees.has(target)) { + return generatedTrees.get(target) as InputTypeClass; + } + + const inputType = getInputType(target); + + if (!inputType) { + generatedTrees.set(target, null); + + return null; + } + + const generate = (meta: ClassMetadata): InputTypeClass => { + const value: InputTypeClass = { + target: meta.target as ClassType, + fields: (meta.fields || []).map(field => { + const fieldInputType = getInputType(field.getType() as ClassType); + + return { + name: field.name, + fields: fieldInputType ? generate(fieldInputType) : undefined, + isArray: field.typeOptions.array === true, + }; + }), + }; + + const superPrototype = Object.getPrototypeOf(meta.target); + if (superPrototype) { + const superInputType = getInputType(superPrototype); + if (superInputType) { + value.fields = value.fields.concat(generate(superInputType).fields); + } + } + + return value; + }; + + const tree = generate(inputType); + + generatedTrees.set(target, tree); + + return tree; +} + +function convertToInput(tree: InputTypeClass, data?: any) { + const input = new tree.target(); + + tree.fields.forEach(field => { + if (typeof field.fields !== "undefined") { + const siblings = field.fields; + + if (field.isArray) { + input[field.name] = (data[field.name] || []).map((value: any) => + convertToInput(siblings, value), + ); + } else { + input[field.name] = convertToInput(siblings, data[field.name]); + } + } else { + input[field.name] = data[field.name]; + } + }); + + return input; +} + +export function convertToArg(param: ArgParamMetadata, args: ArgsDictionary) { + const tree = generateTree(param); + + return tree + ? convertToInput(tree, args[param.name]) + : convertToType(param.getType(), args[param.name]); +} diff --git a/src/resolvers/helpers.ts b/src/resolvers/helpers.ts index c21eb9728..158997561 100644 --- a/src/resolvers/helpers.ts +++ b/src/resolvers/helpers.ts @@ -1,6 +1,5 @@ import { PubSubEngine } from "graphql-subscriptions"; import { ValidatorOptions } from "class-validator"; - import { ParamMetadata } from "../metadata/definitions"; import { convertToType } from "../helpers/types"; import { validateArg } from "./validate-arg"; @@ -8,6 +7,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 { convertToArg } from "./convert-arg"; export async function getParams( params: ParamMetadata[], @@ -28,7 +28,7 @@ export async function getParams( ); case "arg": return await validateArg( - convertToType(paramInfo.getType(), resolverData.args[paramInfo.name]), + convertToArg(paramInfo, resolverData.args), globalValidate, paramInfo.validate, ); diff --git a/tests/functional/resolvers.ts b/tests/functional/resolvers.ts index 9af4fc27f..c0553f7e0 100644 --- a/tests/functional/resolvers.ts +++ b/tests/functional/resolvers.ts @@ -2053,4 +2053,83 @@ describe("Resolvers", () => { expect(thisVar).toBeInstanceOf(childResolver); }); }); + + it("nested inputs", async () => { + getMetadataStorage().clear(); + + @InputType() + class NestedBaseInput { + @Field() + nestedBaseInputField: string; + } + + @InputType() + class SampleBaseInput { + @Field() + baseInputField: string; + + @Field() + nestedBaseInputField: NestedBaseInput; + + @Field(() => [NestedBaseInput]) + nestedBaseInputFieldArray: NestedBaseInput[]; + } + + @InputType() + class NestedInput { + @Field() + inputField: string; + } + + @InputType() + class SampleInput extends SampleBaseInput { + @Field() + nested: NestedInput; + + @Field(() => [NestedInput]) + nestedArray: NestedInput[]; + } + + let input: SampleInput | undefined; + + @Resolver() + class SampleResolver { + @Query() + sampleQuery(@Arg("input") i: SampleInput): string { + input = i; + + return "sampleQuery"; + } + } + + const schema = await buildSchema({ resolvers: [SampleResolver] }); + + const query = `query { + sampleQuery(input: { + baseInputField: "base input field" + nestedBaseInputField: { nestedBaseInputField: "nested base input field value" } + nestedBaseInputFieldArray: [{ nestedBaseInputField: "nested base input field value" }] + nested: { inputField: "value 1" } + nestedArray: [{ inputField: "value 2" }] + }) + }`; + + await graphql(schema, query); + + expect(input).toBeInstanceOf(SampleInput); + expect((input as SampleInput).nested).toBeInstanceOf(NestedInput); + expect((input as SampleInput).baseInputField).toBe("base input field"); + expect((input as SampleInput).nestedBaseInputField).toBeInstanceOf(NestedBaseInput); + expect((input as SampleInput).nestedBaseInputField.nestedBaseInputField).toBe( + "nested base input field value", + ); + expect((input as SampleInput).nestedBaseInputFieldArray).toHaveLength(1); + expect((input as SampleInput).nestedBaseInputFieldArray[0]).toBeInstanceOf(NestedBaseInput); + expect((input as SampleInput).nestedBaseInputFieldArray[0].nestedBaseInputField).toBe( + "nested base input field value", + ); + expect((input as SampleInput).nestedArray).toHaveLength(1); + expect((input as SampleInput).nestedArray[0]).toBeInstanceOf(NestedInput); + expect((input as SampleInput).nestedArray[0].inputField).toBe("value 2"); + }); });