diff --git a/src/__tests__/recursive-input.test.ts b/src/__tests__/recursive-input.test.ts new file mode 100644 index 00000000..7dee3c18 --- /dev/null +++ b/src/__tests__/recursive-input.test.ts @@ -0,0 +1,435 @@ +import { DocumentNode, GraphQLSchema, parse, validate } from "graphql"; +import { makeExecutableSchema } from "@graphql-tools/schema"; +import { compileQuery, isCompiledQuery } from "../execution"; + +describe("recursive input types", () => { + describe("simple recursive input", () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + foo(input: FooInput): String + } + input FooInput { + foo: FooInput + } + `, + resolvers: { + Query: { + foo(_, args) { + // used as the actual value in test matchers + return JSON.stringify(args); + } + } + } + }); + + test("should not fail for recursive input without variables", () => { + const query = parse(` + { + foo(input: { + foo: { + foo: { + foo: { + foo: {} + } + } + } + }) + } + `); + + const result = executeQuery(schema, query); + + expect(result.errors).toBeUndefined(); + expect(result.data.foo).toBe( + JSON.stringify({ + input: { + foo: { foo: { foo: { foo: {} } } } + } + }) + ); + }); + + test("should not fail with variables using recursive input types", () => { + const document = parse(` + query ($f: FooInput) { + foo(input: $f) + } + `); + const variables = { + f: { + foo: { foo: { foo: {} } } + } + }; + + const result = executeQuery(schema, document, variables); + expect(result.errors).toBeUndefined(); + expect(result.data.foo).toBe( + JSON.stringify({ + input: { foo: { foo: { foo: {} } } } + }) + ); + }); + + // when the recursive variable appers at a nested level + test("should not fail with variables using recursive input types - 2", () => { + const document = parse(` + query ($f: FooInput) { + foo(input: { + foo: { foo: { foo: $f } } + }) + } + `); + const variables = { + f: { + foo: { foo: { foo: {} } } + } + }; + + const result = executeQuery(schema, document, variables); + expect(result.errors).toBeUndefined(); + expect(result.data.foo).toBe( + JSON.stringify({ + input: { foo: { foo: { foo: { foo: { foo: { foo: {} } } } } } } + }) + ); + }); + + test("should work with multiple variables using the same recursive input type", () => { + const document = parse(` + query ($f: FooInput, $g: FooInput) { + a: foo(input: $f) + b: foo(input: $g) + } + `); + const variables = { + f: { + foo: { foo: { foo: {} } } + }, + g: { + foo: {} + } + }; + + const result = executeQuery(schema, document, variables); + expect(result.errors).toBeUndefined(); + expect(result.data.a).toBe( + JSON.stringify({ + input: { foo: { foo: { foo: {} } } } + }) + ); + expect(result.data.b).toBe( + JSON.stringify({ + input: { foo: {} } + }) + ); + }); + + test("should work with multiple variables using the same recursive input type - 2 (reverse order)", () => { + const document = parse(` + query ($f: FooInput, $g: FooInput) { + a: foo(input: $g) + b: foo(input: $f) + } + `); + const variables = { + g: { + foo: {} + }, + f: { + foo: { foo: { foo: {} } } + } + }; + + const result = executeQuery(schema, document, variables); + expect(result.errors).toBeUndefined(); + expect(result.data.b).toBe( + JSON.stringify({ + input: { foo: { foo: { foo: {} } } } + }) + ); + expect(result.data.a).toBe( + JSON.stringify({ + input: { foo: {} } + }) + ); + }); + }); + + describe("simple recursive input - 2", () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + foo(input: FooInput): String + } + input FooInput { + foo: FooInput + bar: String + } + `, + resolvers: { + Query: { + foo(_, args) { + // used as the actual value in test matchers + return JSON.stringify(args); + } + } + } + }); + + test("should nòt fail for same leaf values", () => { + const document = parse(` + query ($f: FooInput) { + foo(input: $f) + } + `); + const variables = { + f: { + foo: { + bar: "bar" + }, + bar: "bar" + } + }; + + const result = executeQuery(schema, document, variables); + expect(result.errors).toBeUndefined(); + expect(JSON.parse(result.data.foo).input).toEqual(variables.f); + }); + }); + + describe("mutually recursive input types", () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + products(filter: Filter): String + } + input Filter { + and: AndFilter + or: OrFilter + like: String + } + input AndFilter { + left: Filter + right: Filter + } + input OrFilter { + left: Filter + right: Filter + } + `, + resolvers: { + Query: { + products(_, args) { + // used as the actual value in test matchers + return JSON.stringify(args); + } + } + } + }); + + test("should not fail for mutually recursive variables", () => { + const document = parse(` + query ($filter1: Filter) { + products(filter: $filter1) + } + `); + + const variables = { + filter1: { + and: { + left: { + like: "windows" + }, + right: { + or: { + left: { + like: "xp" + }, + right: { + like: "vista" + } + } + } + } + } + }; + + const result = executeQuery(schema, document, variables); + expect(JSON.parse(result.data.products).filter).toEqual( + variables.filter1 + ); + }); + + test("should not fail for mutually recursive variables - multiple variables", () => { + const document = parse(` + query ($aFilter: Filter, $bFilter: Filter) { + a: products(filter: $aFilter) + b: products(filter: $bFilter) + } + `); + + const variables = { + aFilter: { + and: { + left: { + like: "windows" + }, + right: { + or: { + left: { + like: "xp" + }, + right: { + like: "vista" + } + } + } + } + }, + bFilter: { + like: "mac", + or: { + left: { + like: "10" + }, + right: { + like: "11" + } + } + } + }; + + const result = executeQuery(schema, document, variables); + expect(JSON.parse(result.data.a).filter).toEqual(variables.aFilter); + expect(JSON.parse(result.data.b).filter).toEqual(variables.bFilter); + }); + + // when the mutually recursive input type appears at nested level + // instead of the top-level variable + test("should not fail for mutually recursive variables - 2", () => { + const document = parse(` + query ($macFilter: OrFilter) { + products(filter: { + like: "mac" + and: { + left: { like: "User" } + right: { like: "foo" } + } + or: $macFilter + }) + } + `); + + const variables = { + macFilter: { + left: { like: "Applications/Safari" }, + right: { like: "Applications/Notes" } + } + }; + + const result = executeQuery(schema, document, variables); + expect(JSON.parse(result.data.products).filter.or).toEqual( + variables.macFilter + ); + }); + }); + + describe("lists", () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + items(filters: [Filter]): String + } + input Filter { + or: [Filter] + and: [Filter] + like: String + } + `, + resolvers: { + Query: { + items(_, input) { + // used as the actual value in test matchers + return JSON.stringify(input); + } + } + } + }); + + test("should work with recursive types in lists", () => { + const document = parse(` + query ($filters: [Filter]) { + items(filters: $filters) + } + `); + const variables = { + filters: [ + { + or: [ + { + like: "gallery", + or: [{ like: "photo" }, { like: "video" }] + } + ] + } + ] + }; + + const result = executeQuery(schema, document, variables); + expect(result.errors).toBeUndefined(); + expect(JSON.parse(result.data.items).filters).toEqual(variables.filters); + }); + }); + + describe("lists - 2", () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + flatten(list: [[[[[Item]]]]]): String + } + input Item { + id: ID + } + `, + resolvers: { + Query: { + flatten(_, input) { + // used as the actual value in test matchers + return JSON.stringify(input); + } + } + } + }); + + test("should work with recursive types in lists", () => { + const document = parse(` + query ($list: [[[[[Item]]]]]) { + flatten(list: $list) + } + `); + const variables = { + list: [ + [[[[{ id: "1" }, { id: "2" }]]]], + [[[[{ id: "3" }, { id: "4" }]]]] + ] + }; + + const result = executeQuery(schema, document, variables); + expect(result.errors).toBeUndefined(); + expect(JSON.parse(result.data.flatten).list).toEqual(variables.list); + }); + }); +}); + +function executeQuery( + schema: GraphQLSchema, + document: DocumentNode, + variableValues?: any +) { + const prepared: any = compileQuery(schema, document as any, undefined, {}); + if (!isCompiledQuery(prepared)) { + return prepared; + } + return prepared.query({}, {}, variableValues || {}); +} diff --git a/src/__tests__/subscription.test.ts b/src/__tests__/subscription.test.ts index 765421f5..a7a4c3cd 100644 --- a/src/__tests__/subscription.test.ts +++ b/src/__tests__/subscription.test.ts @@ -488,7 +488,7 @@ describe("Subscription Initialization Phase", () => { { message: // DIFF: 'Variable "$arg" got invalid value "meow"; Int cannot represent non-integer value: "meow"', - 'Variable "$arg" got invalid value "meow"; Expected type Int; Int cannot represent non-integer value: "meow"', + 'Variable "$arg" got invalid value "meow"; Int cannot represent non-integer value: "meow"', locations: [{ line: 2, column: 21 }] } ] diff --git a/src/__tests__/variables.test.ts b/src/__tests__/variables.test.ts index eb94653b..e3355566 100644 --- a/src/__tests__/variables.test.ts +++ b/src/__tests__/variables.test.ts @@ -541,13 +541,13 @@ describe("Execute: Handles inputs", () => { errors: [ { message: - 'Variable "$a" got invalid value "SerializedValue"; Expected type ComplexThrowingScalar.', + 'Variable "$input" got invalid value "SerializedValue" at "input.a"; Expected type "ComplexThrowingScalar". complex-scalar-error', locations: [{ line: 2, column: 16 }] } ] }); expect(result.errors?.[0].originalError?.message).toBe( - "complex-scalar-error" + 'Expected type "ComplexThrowingScalar". complex-scalar-error' ); }); @@ -559,8 +559,7 @@ describe("Execute: Handles inputs", () => { errors: [ { message: - 'Variable "$input" got invalid value { a: "foo", b: "bar", c: null }; ' + - "Expected non-nullable type String! not to be null at value.c.", + 'Variable "$input" got invalid value null at "input.c"; Expected non-nullable type "String!" not to be null.', locations: [{ line: 2, column: 16 }] } ] @@ -574,8 +573,7 @@ describe("Execute: Handles inputs", () => { errors: [ { message: - 'Variable "$input" got invalid value "foo bar"; ' + - "Expected type TestInputObject to be an object.", + 'Variable "$input" got invalid value "foo bar"; Expected type "TestInputObject" to be an object.', locations: [{ line: 2, column: 16 }] } ] @@ -591,8 +589,7 @@ describe("Execute: Handles inputs", () => { errors: [ { message: - 'Variable "$input" got invalid value { a: "foo", b: "bar" }; ' + - "Field value.c of required type String! was not provided.", + 'Variable "$input" got invalid value { a: "foo", b: "bar" }; Field "c" of required type "String!" was not provided.', locations: [{ line: 2, column: 16 }] } ] @@ -609,8 +606,7 @@ describe("Execute: Handles inputs", () => { errors: [ { message: - 'Variable "$input" got invalid value { a: "foo", b: "bar", c: "baz", extra: "dog" }; ' + - 'Field "extra" is not defined by type TestInputObject.', + 'Variable "$input" got invalid value { a: "foo", b: "bar", c: "baz", extra: "dog" }; Field "extra" is not defined by type "TestInputObject".', locations: [{ line: 2, column: 16 }] } ] @@ -802,7 +798,7 @@ describe("Execute: Handles inputs", () => { errors: [ { message: - 'Variable "$input" got invalid value "foo bar"; Expected type TestEnum.', + 'Variable "$input" got invalid value "foo bar"; Value "foo bar" does not exist in "TestEnum" enum.', locations: [{ line: 2, column: 16 }] } ] @@ -998,8 +994,7 @@ describe("Execute: Handles inputs", () => { } ], message: - 'Variable "$int" got invalid value 9007199254740992; Expected type Int; ' + - "Int cannot represent non 32-bit signed integer value: 9007199254740992" + 'Variable "$int" got invalid value 9007199254740992; Int cannot represent non 32-bit signed integer value: 9007199254740992' } ] }); @@ -1024,8 +1019,7 @@ describe("Execute: Handles inputs", () => { } ], message: - 'Variable "$string" got invalid value ["a"]; Expected type String; ' + - 'String cannot represent a non string value: ["a"]' + 'Variable "$string" got invalid value ["a"]; String cannot represent a non string value: ["a"]' }, { locations: [ @@ -1035,8 +1029,7 @@ describe("Execute: Handles inputs", () => { } ], message: - 'Variable "$id" got invalid value ["id"]; Expected type ID; ' + - 'ID cannot represent value: ["id"]' + 'Variable "$id" got invalid value ["id"]; ID cannot represent value: ["id"]' }, { locations: [ @@ -1046,8 +1039,7 @@ describe("Execute: Handles inputs", () => { } ], message: - 'Variable "$int" got invalid value 1.5; Expected type Int; ' + - "Int cannot represent non-integer value: 1.5" + 'Variable "$int" got invalid value 1.5; Int cannot represent non-integer value: 1.5' }, { locations: [ @@ -1057,8 +1049,7 @@ describe("Execute: Handles inputs", () => { } ], message: - 'Variable "$float" got invalid value NaN; Expected type Float; ' + - "Float cannot represent non numeric value: NaN" + 'Variable "$float" got invalid value NaN; Float cannot represent non numeric value: NaN' }, { locations: [ @@ -1068,8 +1059,7 @@ describe("Execute: Handles inputs", () => { } ], message: - 'Variable "$boolean" got invalid value "hello"; Expected type Boolean; ' + - 'Boolean cannot represent a non boolean value: "hello"' + 'Variable "$boolean" got invalid value "hello"; Boolean cannot represent a non boolean value: "hello"' } ] }); @@ -1283,8 +1273,7 @@ describe("Execute: Handles inputs", () => { errors: [ { message: - 'Variable "$value" got invalid value [1, 2, 3]; ' + - "Expected type String; String cannot represent a non string value: [1, 2, 3]", + 'Variable "$value" got invalid value [1, 2, 3]; String cannot represent a non string value: [1, 2, 3]', locations: [{ line: 2, column: 16 }] } ] @@ -1492,8 +1481,7 @@ describe("Execute: Handles inputs", () => { errors: [ { message: - 'Variable "$input" got invalid value ["A", null, "B"]; ' + - "Expected non-nullable type String! not to be null at value[1].", + 'Variable "$input" got invalid value null at "input[1]"; Expected non-nullable type "String!" not to be null.', locations: [{ line: 2, column: 16 }] } ] @@ -1542,8 +1530,7 @@ describe("Execute: Handles inputs", () => { errors: [ { message: - 'Variable "$input" got invalid value ["A", null, "B"]; ' + - "Expected non-nullable type String! not to be null at value[1].", + 'Variable "$input" got invalid value null at "input[1]"; Expected non-nullable type "String!" not to be null.', locations: [{ line: 2, column: 16 }] } ] diff --git a/src/execution.ts b/src/execution.ts index 73b526a8..7dab089d 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -60,7 +60,7 @@ import { import { Maybe } from "./types"; import { CoercedVariableValues, - compileVariableParsing, + getVariablesParser, failToParseVariables } from "./variables"; import { getGraphQLErrorOptions, getOperationRootType } from "./compat"; @@ -259,7 +259,7 @@ export function compileQuery< } else { stringify = JSON.stringify; } - const getVariables = compileVariableParsing( + const getVariables = getVariablesParser( schema, context.operation.variableDefinitions || [] ); diff --git a/src/variables.ts b/src/variables.ts index 582380bc..ef6a1e45 100644 --- a/src/variables.ts +++ b/src/variables.ts @@ -1,29 +1,9 @@ -import genFn from "generate-function"; import { - GraphQLBoolean, + getVariableValues, GraphQLError, - GraphQLFloat, - GraphQLID, - GraphQLInputType, - GraphQLInt, GraphQLSchema, - GraphQLString, - isEnumType, - isInputType, - isListType, - isNonNullType, - isScalarType, - print, - SourceLocation, - typeFromAST, - valueFromAST, VariableDefinitionNode } from "graphql"; -import { addPath, computeLocations, ObjectPath } from "./ast"; -import { GraphQLError as GraphQLJITError } from "./error"; -import createInspect from "./inspect"; - -const inspect = createInspect(); interface FailedVariableCoercion { errors: ReadonlyArray; @@ -39,420 +19,9 @@ export function failToParseVariables(x: any): x is FailedVariableCoercion { return x.errors; } -interface CompilationContext { - inputPath: ObjectPath; - responsePath: ObjectPath; - depth: number; - varDefNode: VariableDefinitionNode; - dependencies: Map any>; - errorMessage?: string; -} - -function createSubCompilationContext( - context: CompilationContext -): CompilationContext { - return { ...context }; -} -export function compileVariableParsing( +export function getVariablesParser( schema: GraphQLSchema, varDefNodes: ReadonlyArray ): (inputs: { [key: string]: any }) => CoercedVariableValues { - const errors = []; - const coercedValues: { [key: string]: any } = Object.create(null); - - let mainBody = ""; - const dependencies = new Map(); - for (const varDefNode of varDefNodes) { - const context: CompilationContext = { - varDefNode, - depth: 0, - inputPath: addPath(undefined, "input"), - responsePath: addPath(undefined, "coerced"), - dependencies - }; - const varName = varDefNode.variable.name.value; - const varType = typeFromAST(schema, varDefNode.type as any); - if (!varType || !isInputType(varType)) { - // Must use input types for variables. This should be caught during - // validation, however is checked again here for safety. - errors.push( - new (GraphQLJITError as any)( - `Variable "$${varName}" expected value of type ` + - `"${ - varType || print(varDefNode.type) - }" which cannot be used as an input type.`, - computeLocations([varDefNode.type]) - ) - ); - continue; - } - - // Ensure a constant shape of the input map - coercedValues[varName] = undefined; - const hasValueName = hasValue(addPath(context.inputPath, varName)); - mainBody += `const ${hasValueName} = Object.prototype.hasOwnProperty.call(${getObjectPath( - context.inputPath - )}, "${varName}");\n`; - context.inputPath = addPath(context.inputPath, varName); - context.responsePath = addPath(context.responsePath, varName); - mainBody += generateInput( - context, - varType, - varName, - hasValueName, - valueFromAST(varDefNode.defaultValue, varType), - false - ); - } - - if (errors.length > 0) { - throw errors; - } - - const gen = genFn(); - gen(` - return function getVariables(input) { - const errors = []; - const coerced = ${JSON.stringify(coercedValues)} - ${mainBody} - if (errors.length > 0) { - return {errors, coerced: undefined}; - } - return {errors: undefined, coerced}; - } - `); - - // eslint-disable-next-line - return Function.apply( - null, - ["GraphQLJITError", "inspect"] - .concat(Array.from(dependencies.keys())) - .concat(gen.toString()) - ).apply( - null, - [GraphQLJITError, inspect].concat(Array.from(dependencies.values())) - ); -} - -// Int Scalars represent 32 bits -// https://graphql.github.io/graphql-spec/June2018/#sec-Int -const MAX_32BIT_INT = 2147483647; -const MIN_32BIT_INT = -2147483648; - -function generateInput( - context: CompilationContext, - varType: GraphQLInputType, - varName: string, - hasValueName: string, - defaultValue: unknown | undefined, - wrapInList: boolean -) { - const currentOutput = getObjectPath(context.responsePath); - const currentInput = getObjectPath(context.inputPath); - const errorLocation = printErrorLocation( - computeLocations([context.varDefNode]) - ); - - const gen = genFn(); - gen(`if (${currentInput} == null) {`); - - if (isNonNullType(varType)) { - let nonNullMessage; - let omittedMessage; - if (context.errorMessage) { - const objectPath = printObjectPath(context.responsePath); - nonNullMessage = `${context.errorMessage} + \`Expected non-nullable type ${varType} not to be null at ${objectPath}.\``; - omittedMessage = `${context.errorMessage} + \`Field ${objectPath} of required type ${varType} was not provided.\``; - } else { - nonNullMessage = `'Variable "$${varName}" of non-null type "${varType}" must not be null.'`; - omittedMessage = `'Variable "$${varName}" of required type "${varType}" was not provided.'`; - } - varType = varType.ofType; - gen(` - if (${currentOutput} == null) { - errors.push(new GraphQLJITError(${hasValueName} ? ${nonNullMessage} : ${omittedMessage}, ${errorLocation})); - } - `); - } else { - gen(` - if (${hasValueName}) { ${currentOutput} = null; } - `); - if (defaultValue !== undefined) { - gen(`else { ${currentOutput} = ${JSON.stringify(defaultValue)} }`); - } - } - gen(`} else {`); - if (isScalarType(varType)) { - switch (varType.name) { - case GraphQLID.name: - gen(` - if (typeof ${currentInput} === "string") { - ${currentOutput} = ${currentInput}; - } else if (Number.isInteger(${currentInput})) { - ${currentOutput} = ${currentInput}.toString(); - } else { - errors.push(new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name}; ' + - '${varType.name} cannot represent value: ' + - inspect(${currentInput}), ${errorLocation}) - ); - } - `); - break; - case GraphQLString.name: - gen(` - if (typeof ${currentInput} === "string") { - ${currentOutput} = ${currentInput}; - } else { - errors.push(new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name}; ' + - '${varType.name} cannot represent a non string value: ' + - inspect(${currentInput}), ${errorLocation}) - ); - } - `); - break; - case GraphQLBoolean.name: - gen(` - if (typeof ${currentInput} === "boolean") { - ${currentOutput} = ${currentInput}; - } else { - errors.push(new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name}; ' + - '${varType.name} cannot represent a non boolean value: ' + - inspect(${currentInput}), ${errorLocation})); - } - `); - break; - case GraphQLInt.name: - gen(` - if (Number.isInteger(${currentInput})) { - if (${currentInput} > ${MAX_32BIT_INT} || ${currentInput} < ${MIN_32BIT_INT}) { - errors.push(new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name}; ' + - '${varType.name} cannot represent non 32-bit signed integer value: ' + - inspect(${currentInput}), ${errorLocation})); - } else { - ${currentOutput} = ${currentInput}; - } - } else { - errors.push(new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name}; ' + - '${varType.name} cannot represent non-integer value: ' + - inspect(${currentInput}), ${errorLocation}) - ); - } - `); - break; - case GraphQLFloat.name: - gen(` - if (Number.isFinite(${currentInput})) { - ${currentOutput} = ${currentInput}; - } else { - errors.push(new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name}; ' + - '${varType.name} cannot represent non numeric value: ' + - inspect(${currentInput}), ${errorLocation}) - ); - } - `); - break; - default: - context.dependencies.set( - `${varType.name}parseValue`, - varType.parseValue.bind(varType) - ); - gen(` - try { - const parseResult = ${varType.name}parseValue(${currentInput}); - if (parseResult === undefined || parseResult !== parseResult) { - errors.push(new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name}.', ${errorLocation})); - } - ${currentOutput} = parseResult; - } catch (error) { - errors.push(new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name}.', ${errorLocation}, undefined, error) - ); - } - `); - } - } else if (isEnumType(varType)) { - context.dependencies.set( - `${varType.name}getValue`, - varType.getValue.bind(varType) - ); - gen(` - if (typeof ${currentInput} === "string") { - const enumValue = ${varType.name}getValue(${currentInput}); - if (enumValue) { - ${currentOutput} = enumValue.value; - } else { - errors.push( - new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name}.', ${errorLocation}) - ); - } - } else { - errors.push( - new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name}.', ${errorLocation}) - ); - } - `); - } else if (isListType(varType)) { - context.errorMessage = `'Variable "$${varName}" got invalid value ' + inspect(${currentInput}) + '; '`; - const hasValueName = hasValue(context.inputPath); - const index = `idx${context.depth}`; - - const subContext = createSubCompilationContext(context); - subContext.responsePath = addPath( - subContext.responsePath, - index, - "variable" - ); - subContext.inputPath = addPath(subContext.inputPath, index, "variable"); - subContext.depth++; - gen(` - if (Array.isArray(${currentInput})) { - ${currentOutput} = []; - for (let ${index} = 0; ${index} < ${currentInput}.length; ++${index}) { - const ${hasValueName} = - ${getObjectPath(subContext.inputPath)} !== undefined; - ${generateInput( - subContext, - varType.ofType, - varName, - hasValueName, - undefined, - false - )} - } - } else { - ${generateInput( - context, - varType.ofType, - varName, - hasValueName, - undefined, - true - )} - } - `); - } else if (isInputType(varType)) { - gen(` - if (typeof ${currentInput} !== 'object') { - errors.push(new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Expected type ${varType.name} to be an object.', ${errorLocation})); - } else { - ${currentOutput} = {}; - `); - const fields = varType.getFields(); - const allowedFields = []; - for (const field of Object.values(fields)) { - const subContext = createSubCompilationContext(context); - allowedFields.push(field.name); - const hasValueName = hasValue(addPath(subContext.inputPath, field.name)); - gen(` - const ${hasValueName} = Object.prototype.hasOwnProperty.call( - ${getObjectPath(subContext.inputPath)}, "${field.name}" - ); - `); - subContext.inputPath = addPath(subContext.inputPath, field.name); - subContext.responsePath = addPath(subContext.responsePath, field.name); - subContext.errorMessage = `'Variable "$${varName}" got invalid value ' + inspect(${currentInput}) + '; '`; - gen(` - ${generateInput( - subContext, - field.type, - field.name, - hasValueName, - field.defaultValue, - false - )} - `); - } - - gen(` - const allowedFields = ${JSON.stringify(allowedFields)}; - for (const fieldName of Object.keys(${currentInput})) { - if (!allowedFields.includes(fieldName)) { - errors.push(new GraphQLJITError('Variable "$${varName}" got invalid value ' + - inspect(${currentInput}) + "; " + - 'Field "' + fieldName + '" is not defined by type ${ - varType.name - }.', ${errorLocation})); - break; - } - } - }`); - } else { - /* istanbul ignore next line */ - throw new Error(`unknown type: ${varType}`); - } - if (wrapInList) { - gen(`${currentOutput} = [${currentOutput}];`); - } - gen(`}`); - return gen.toString(); -} - -function hasValue(path: ObjectPath) { - const flattened = []; - let curr: ObjectPath | undefined = path; - while (curr) { - flattened.push(curr.key); - curr = curr.prev; - } - return `hasValue${flattened.join("_")}`; -} - -function printErrorLocation(location: SourceLocation[]) { - return JSON.stringify(location); -} - -function getObjectPath(path: ObjectPath): string { - const flattened = []; - let curr: ObjectPath | undefined = path; - while (curr) { - flattened.unshift({ key: curr.key, type: curr.type }); - curr = curr.prev; - } - let name = flattened[0].key; - for (let i = 1; i < flattened.length; ++i) { - name += - flattened[i].type === "literal" - ? `["${flattened[i].key}"]` - : `[${flattened[i].key}]`; - } - return name; -} - -function printObjectPath(path: ObjectPath) { - const flattened = []; - let curr: ObjectPath | undefined = path; - while (curr) { - flattened.unshift({ key: curr.key, type: curr.type }); - curr = curr.prev; - } - const initialIndex = Math.min(flattened.length - 1, 1); - let name = "value"; - for (let i = initialIndex + 1; i < flattened.length; ++i) { - name += - flattened[i].type === "literal" - ? `.${flattened[i].key}` - : `[$\{${flattened[i].key}}]`; - } - return name; + return (inputs) => getVariableValues(schema, varDefNodes, inputs); }