diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index bb9bf60224..1b738a6124 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -3,6 +3,7 @@ import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; +import { identityFunc } from '../../jsutils/identityFunc'; import { inspect } from '../../jsutils/inspect'; import { Kind } from '../../language/kinds'; @@ -1146,6 +1147,93 @@ describe('Execute: Handles basic execution tasks', () => { expect(asyncResult).to.deep.equal(result); }); + it('passes context to custom scalar parseValue', () => { + const customScalar = new GraphQLScalarType({ + name: 'CustomScalar', + parseValue: (_value, context) => context, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + customScalar: { + type: GraphQLInt, + args: { + passThroughContext: { + type: customScalar, + }, + }, + resolve: (_source, _args, context) => context, + }, + }, + }), + }); + + const result = executeSync({ + schema, + document: parse('{ customScalar(passThroughContext: 0) }'), + contextValue: 1, + }); + expectJSON(result).toDeepEqual({ + data: { customScalar: 1 }, + }); + }); + + it('passes context to custom scalar serialize', () => { + const customScalar = new GraphQLScalarType({ + name: 'CustomScalar', + serialize: (_value, context) => context, + parseValue: identityFunc, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + customScalar: { + type: customScalar, + resolve: () => 'CUSTOM_VALUE', + }, + }, + }), + }); + + const result = executeSync({ + schema, + document: parse('{ customScalar }'), + contextValue: 1, + }); + expectJSON(result).toDeepEqual({ + data: { customScalar: 1 }, + }); + }); + + it('passes context to custom scalar serialize', () => { + const customScalar = new GraphQLScalarType({ + name: 'CustomScalar', + serialize: (_value, context) => context, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + customScalar: { + type: customScalar, + resolve: () => 'CUSTOM_VALUE', + }, + }, + }), + }); + + const result = executeSync({ + schema, + document: parse('{ customScalar }'), + contextValue: 1, + }); + expectJSON(result).toDeepEqual({ + data: { customScalar: 1 }, + }); + }); + it('fails when serialize of custom scalar does not return a value', () => { const customScalar = new GraphQLScalarType({ name: 'CustomScalar', diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index f21bb95032..c8ace1e17c 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -38,6 +38,11 @@ const TestComplexScalar = new GraphQLScalarType({ }, }); +const TestContextScalar = new GraphQLScalarType({ + name: 'ContextScalar', + parseValue: (_value, context) => context, +}); + const TestInputObject = new GraphQLInputObjectType({ name: 'TestInputObject', fields: { @@ -45,6 +50,7 @@ const TestInputObject = new GraphQLInputObjectType({ b: { type: new GraphQLList(GraphQLString) }, c: { type: new GraphQLNonNull(GraphQLString) }, d: { type: TestComplexScalar }, + e: { type: TestContextScalar }, }, }); @@ -126,9 +132,10 @@ const schema = new GraphQLSchema({ query: TestType }); function executeQuery( query: string, variableValues?: { [variable: string]: unknown }, + contextValue?: unknown, ) { const document = parse(query); - return executeSync({ schema, document, variableValues }); + return executeSync({ schema, document, contextValue, variableValues }); } describe('Execute: Handles inputs', () => { @@ -366,6 +373,18 @@ describe('Execute: Handles inputs', () => { }); }); + it('executes with scalar access to context', () => { + const params = { input: { c: 'foo', e: false } }; + const contextValue = true; + const result = executeQuery(doc, params, contextValue); + + expect(result).to.deep.equal({ + data: { + fieldWithObjectInput: '{ c: "foo", e: true }', + }, + }); + }); + it('errors on null for nested non-null', () => { const params = { input: { a: 'foo', b: 'bar', c: null } }; const result = executeQuery(doc, params); @@ -1044,6 +1063,7 @@ describe('Execute: Handles inputs', () => { schema, variableDefinitions, inputValue, + undefined, { maxErrors: 3 }, ); @@ -1061,6 +1081,7 @@ describe('Execute: Handles inputs', () => { schema, variableDefinitions, inputValue, + undefined, { maxErrors: 2 }, ); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 3261f51184..abf60f96ec 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -323,6 +323,7 @@ export function buildExecutionContext( schema, variableDefinitions, rawVariableValues ?? {}, + contextValue, { maxErrors: 50 }, ); @@ -497,6 +498,11 @@ function executeField( path, ); + // The resolve function's optional third argument is a context value that + // is provided to every resolve function within an execution. It is commonly + // used to represent an authenticated user, or request-specific caches. + const { contextValue, variableValues } = exeContext; + // Get the resolve function, regardless of if its result is normal or abrupt (error). try { // Build a JS object of arguments from the field.arguments AST, using the @@ -505,14 +511,10 @@ function executeField( const args = getArgumentValues( fieldDef, fieldNodes[0], - exeContext.variableValues, + contextValue, + variableValues, ); - // The resolve function's optional third argument is a context value that - // is provided to every resolve function within an execution. It is commonly - // used to represent an authenticated user, or request-specific caches. - const contextValue = exeContext.contextValue; - const result = resolveFn(source, args, contextValue, info); let completed; @@ -662,7 +664,7 @@ function completeValue( // If field type is a leaf type, Scalar or Enum, serialize to a valid value, // returning null if serialization is not possible. if (isLeafType(returnType)) { - return completeLeafValue(returnType, result); + return completeLeafValue(exeContext, returnType, result); } // If field type is an abstract type, Interface or Union, determine the @@ -775,10 +777,14 @@ function completeListValue( * null if serialization is not possible. */ function completeLeafValue( + exeContext: ExecutionContext, returnType: GraphQLLeafType, result: unknown, ): unknown { - const serializedResult = returnType.serialize(result); + const serializedResult = returnType.serialize( + result, + exeContext.contextValue, + ); if (serializedResult == null) { throw new Error( `Expected \`${inspect(returnType)}.serialize(${inspect(result)})\` to ` + diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts index 0b240b3fd7..6806f325a4 100644 --- a/src/execution/subscribe.ts +++ b/src/execution/subscribe.ts @@ -180,8 +180,17 @@ export async function createSourceEventStream( async function executeSubscription( exeContext: ExecutionContext, ): Promise { - const { schema, fragments, operation, variableValues, rootValue } = - exeContext; + // The resolve function's optional third argument is a context value that + // is provided to every resolve function within an execution. It is commonly + // used to represent an authenticated user, or request-specific caches. + const { + schema, + fragments, + operation, + contextValue, + variableValues, + rootValue, + } = exeContext; const rootType = schema.getSubscriptionType(); if (rootType == null) { @@ -224,12 +233,12 @@ async function executeSubscription( // Build a JS object of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. - const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues); - - // The resolve function's optional third argument is a context value that - // is provided to every resolve function within an execution. It is commonly - // used to represent an authenticated user, or request-specific caches. - const contextValue = exeContext.contextValue; + const args = getArgumentValues( + fieldDef, + fieldNodes[0], + contextValue, + variableValues, + ); // Call the `subscribe()` resolver or the default resolver to produce an // AsyncIterable yielding raw payloads. diff --git a/src/execution/values.ts b/src/execution/values.ts index 023e028109..9db45d44f2 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -36,10 +36,11 @@ type CoercedVariableValues = * exposed to user code. Care should be taken to not pull values from the * Object prototype. */ -export function getVariableValues( +export function getVariableValues( schema: GraphQLSchema, varDefNodes: ReadonlyArray, inputs: { readonly [variable: string]: unknown }, + context?: Maybe, options?: { maxErrors?: number }, ): CoercedVariableValues { const errors = []; @@ -47,6 +48,7 @@ export function getVariableValues( try { const coerced = coerceVariableValues( schema, + context, varDefNodes, inputs, (error) => { @@ -69,8 +71,9 @@ export function getVariableValues( return { errors }; } -function coerceVariableValues( +function coerceVariableValues( schema: GraphQLSchema, + context: TContext, varDefNodes: ReadonlyArray, inputs: { readonly [variable: string]: unknown }, onError: (error: GraphQLError) => void, @@ -122,6 +125,7 @@ function coerceVariableValues( coercedValues[varName] = coerceInputValue( value, varType, + context, (path, invalidValue, error) => { let prefix = `Variable "$${varName}" got invalid value ` + inspect(invalidValue); @@ -149,9 +153,10 @@ function coerceVariableValues( * exposed to user code. Care should be taken to not pull values from the * Object prototype. */ -export function getArgumentValues( +export function getArgumentValues( def: GraphQLField | GraphQLDirective, node: FieldNode | DirectiveNode, + context?: TContext, variableValues?: Maybe>, ): { [argument: string]: unknown } { const coercedValues: { [argument: string]: unknown } = {}; @@ -210,7 +215,12 @@ export function getArgumentValues( ); } - const coercedValue = valueFromAST(valueNode, argType, variableValues); + const coercedValue = valueFromAST( + valueNode, + argType, + context, + variableValues, + ); if (coercedValue === undefined) { // Note: ValuesOfCorrectTypeRule validation should catch this before // execution. This is a runtime check to ensure execution does not diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts index 19d482915a..12fc505e3b 100644 --- a/src/type/__tests__/definition-test.ts +++ b/src/type/__tests__/definition-test.ts @@ -88,10 +88,25 @@ describe('Type System: Scalars', () => { 'parseValue: { foo: "bar" }', ); expect( - scalar.parseLiteral(parseValue('{ foo: { bar: $var } }'), { var: 'baz' }), + scalar.parseLiteral(parseValue('{ foo: { bar: $var } }'), undefined, { + var: 'baz', + }), ).to.equal('parseValue: { foo: { bar: "baz" } }'); }); + it('pass context to scalar methods', () => { + const scalar = new GraphQLScalarType({ + name: 'Foo', + serialize: (_value, context) => context, + parseValue: (_value, context) => context, + parseLiteral: (_value, context) => context, + }); + + expect(scalar.serialize(undefined, 1)).to.equal(1); + expect(scalar.parseValue(undefined, 1)).to.equal(1); + expect(scalar.parseLiteral(parseValue('null'), 1)).to.equal(1); + }); + it('rejects a Scalar type without name', () => { // @ts-expect-error expect(() => new GraphQLScalarType({})).to.throw('Must provide name.'); diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index df431dafd3..9867d9f606 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -9,6 +9,7 @@ import { getIntrospectionQuery } from '../../utilities/getIntrospectionQuery'; import { graphqlSync } from '../../graphql'; import type { GraphQLResolveInfo } from '../definition'; +import { assertScalarType } from '../definition'; describe('Introspection', () => { it('executes an introspection query', () => { @@ -1087,6 +1088,7 @@ describe('Introspection', () => { input InputObjectWithDefaultValues { a: String = "Emoji: \\u{1F600}" b: Complex = { x: ["abc"], y: 123 } + c: ContextScalar = false } input Complex { @@ -1094,11 +1096,19 @@ describe('Introspection', () => { y: Int } + scalar ContextScalar + type Query { someField(someArg: InputObjectWithDefaultValues): String } `); + // FIXME: workaround since we can't inject serialized into SDL + assertScalarType(schema.getType('ContextScalar')).serialize = ( + _value, + context, + ) => context; + const source = ` { __type(name: "InputObjectWithDefaultValues") { @@ -1110,7 +1120,7 @@ describe('Introspection', () => { } `; - expect(graphqlSync({ schema, source })).to.deep.equal({ + expect(graphqlSync({ schema, source, contextValue: 1 })).to.deep.equal({ data: { __type: { inputFields: [ @@ -1122,6 +1132,10 @@ describe('Introspection', () => { name: 'b', defaultValue: '{ x: ["abc"], y: 123 }', }, + { + name: 'c', + defaultValue: '1', + }, ], }, }, diff --git a/src/type/definition.ts b/src/type/definition.ts index 9eea02e8ea..1a7b736200 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -554,13 +554,17 @@ export interface GraphQLScalarTypeExtensions { * }); * ``` */ -export class GraphQLScalarType { +export class GraphQLScalarType< + TInternal = unknown, + TExternal = TInternal, + TContext = any, +> { name: string; description: Maybe; specifiedByURL: Maybe; - serialize: GraphQLScalarSerializer; - parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; + serialize: GraphQLScalarSerializer; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; extensions: Readonly; astNode: Maybe; extensionASTNodes: ReadonlyArray; @@ -578,7 +582,8 @@ export class GraphQLScalarType { this.parseValue = parseValue; this.parseLiteral = config.parseLiteral ?? - ((node, variables) => parseValue(valueFromASTUntyped(node, variables))); + ((node, context, variables) => + parseValue(valueFromASTUntyped(node, variables), context)); this.extensions = toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; @@ -631,16 +636,19 @@ export class GraphQLScalarType { } } -export type GraphQLScalarSerializer = ( +export type GraphQLScalarSerializer = ( outputValue: unknown, + context?: Maybe, ) => TExternal; -export type GraphQLScalarValueParser = ( +export type GraphQLScalarValueParser = ( inputValue: unknown, + context?: Maybe, ) => TInternal; -export type GraphQLScalarLiteralParser = ( +export type GraphQLScalarLiteralParser = ( valueNode: ValueNode, + context?: Maybe, variables?: Maybe>, ) => TInternal; @@ -1401,6 +1409,7 @@ export class GraphQLEnumType /* */ { parseLiteral( valueNode: ValueNode, + _context: unknown, _variables: Maybe>, ): Maybe /* T */ { // Note: variables will be resolved to a value before calling this function. diff --git a/src/type/introspection.ts b/src/type/introspection.ts index e5fce6f241..32193e3248 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -393,9 +393,9 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ type: GraphQLString, description: 'A GraphQL-formatted string representing the default value for this input value.', - resolve(inputValue) { + resolve(inputValue, _args, context) { const { type, defaultValue } = inputValue; - const valueAST = astFromValue(defaultValue, type); + const valueAST = astFromValue(defaultValue, type, context); return valueAST ? print(valueAST) : null; }, }, diff --git a/src/utilities/__tests__/astFromValue-test.ts b/src/utilities/__tests__/astFromValue-test.ts index b8f2361bd7..d02c9f1edc 100644 --- a/src/utilities/__tests__/astFromValue-test.ts +++ b/src/utilities/__tests__/astFromValue-test.ts @@ -212,6 +212,18 @@ describe('astFromValue', () => { 'Cannot convert value to AST: Infinity.', ); + const contextScalar = new GraphQLScalarType({ + name: 'ContextScalar', + serialize(_value, context) { + return context; + }, + }); + + expect(astFromValue('value', contextScalar, 1)).to.deep.equal({ + kind: 'IntValue', + value: '1', + }); + const returnNullScalar = new GraphQLScalarType({ name: 'ReturnNullScalar', serialize() { diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index a73cdc6116..deb03d6ef9 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -27,11 +27,13 @@ interface CoerceError { function coerceValue( inputValue: unknown, type: GraphQLInputType, + context?: unknown, ): CoerceResult { const errors: Array = []; const value = coerceInputValue( inputValue, type, + context, (path, invalidValue, error) => { errors.push({ path, value: invalidValue, error: error.message }); }, @@ -84,10 +86,13 @@ describe('coerceInputValue', () => { describe('for GraphQLScalar', () => { const TestScalar = new GraphQLScalarType({ name: 'TestScalar', - parseValue(input: any) { + parseValue(input: any, context: any) { if (input.error != null) { throw new Error(input.error); } + if (input.context != null) { + return context; + } return input.value; }, }); @@ -129,6 +134,22 @@ describe('coerceInputValue', () => { }, ]); }); + + it('accesses context when directed', () => { + const inputValue = { context: true }; + const result = coerceValue(inputValue, TestScalar, 1); + expectValue(result).to.equal(1); + }); + + const ContextScalar = new GraphQLScalarType({ + name: 'ContextScalar', + parseValue: (_input, context) => context, + }); + + it('accesses context', () => { + const result = coerceValue({ value: 1 }, ContextScalar, 1); + expectValue(result).to.equal(1); + }); }); describe('for GraphQLEnum', () => { @@ -409,7 +430,7 @@ describe('coerceInputValue', () => { describe('with default onError', () => { it('throw error without path', () => { expect(() => - coerceInputValue(null, new GraphQLNonNull(GraphQLInt)), + coerceInputValue(null, new GraphQLNonNull(GraphQLInt), undefined), ).to.throw( 'Invalid value null: Expected non-nullable type "Int!" not to be null.', ); @@ -420,6 +441,7 @@ describe('coerceInputValue', () => { coerceInputValue( [null], new GraphQLList(new GraphQLNonNull(GraphQLInt)), + undefined, ), ).to.throw( 'Invalid value null at "value[0]": Expected non-nullable type "Int!" not to be null.', diff --git a/src/utilities/__tests__/valueFromAST-test.ts b/src/utilities/__tests__/valueFromAST-test.ts index 73d0be49d9..0b0cf76a03 100644 --- a/src/utilities/__tests__/valueFromAST-test.ts +++ b/src/utilities/__tests__/valueFromAST-test.ts @@ -28,10 +28,11 @@ describe('valueFromAST', () => { function expectValueFrom( valueText: string, type: GraphQLInputType, + context?: unknown, variables?: ObjMap, ) { const ast = parseValue(valueText); - const value = valueFromAST(ast, type, variables); + const value = valueFromAST(ast, type, context, variables); return expect(value); } @@ -73,6 +74,14 @@ describe('valueFromAST', () => { expectValueFrom('"value"', passthroughScalar).to.equal('value'); + const contextScalar = new GraphQLScalarType({ + name: 'ContextScalar', + parseLiteral: (_node, context) => context, + parseValue: identityFunc, + }); + + expectValueFrom('"value"', contextScalar, 1).to.equal(1); + const throwScalar = new GraphQLScalarType({ name: 'ThrowScalar', parseLiteral() { @@ -223,24 +232,36 @@ describe('valueFromAST', () => { }); it('accepts variable values assuming already coerced', () => { - expectValueFrom('$var', GraphQLBoolean, {}).to.equal(undefined); - expectValueFrom('$var', GraphQLBoolean, { var: true }).to.equal(true); - expectValueFrom('$var', GraphQLBoolean, { var: null }).to.equal(null); - expectValueFrom('$var', nonNullBool, { var: null }).to.equal(undefined); + expectValueFrom('$var', GraphQLBoolean, undefined, {}).to.equal(undefined); + expectValueFrom('$var', GraphQLBoolean, undefined, { var: true }).to.equal( + true, + ); + expectValueFrom('$var', GraphQLBoolean, undefined, { var: null }).to.equal( + null, + ); + expectValueFrom('$var', nonNullBool, undefined, { var: null }).to.equal( + undefined, + ); }); it('asserts variables are provided as items in lists', () => { - expectValueFrom('[ $foo ]', listOfBool, {}).to.deep.equal([null]); - expectValueFrom('[ $foo ]', listOfNonNullBool, {}).to.equal(undefined); - expectValueFrom('[ $foo ]', listOfNonNullBool, { + expectValueFrom('[ $foo ]', listOfBool, undefined, {}).to.deep.equal([ + null, + ]); + expectValueFrom('[ $foo ]', listOfNonNullBool, undefined, {}).to.equal( + undefined, + ); + expectValueFrom('[ $foo ]', listOfNonNullBool, undefined, { foo: true, }).to.deep.equal([true]); // Note: variables are expected to have already been coerced, so we // do not expect the singleton wrapping behavior for variables. - expectValueFrom('$foo', listOfNonNullBool, { foo: true }).to.equal(true); - expectValueFrom('$foo', listOfNonNullBool, { foo: [true] }).to.deep.equal([ - true, - ]); + expectValueFrom('$foo', listOfNonNullBool, undefined, { + foo: true, + }).to.equal(true); + expectValueFrom('$foo', listOfNonNullBool, undefined, { + foo: [true], + }).to.deep.equal([true]); }); it('omits input object fields for unprovided variables', () => { @@ -250,11 +271,14 @@ describe('valueFromAST', () => { {}, ).to.deep.equal({ int: 42, requiredBool: true }); - expectValueFrom('{ requiredBool: $foo }', testInputObj, {}).to.equal( + expectValueFrom( + '{ requiredBool: $foo }', + testInputObj, undefined, - ); + {}, + ).to.equal(undefined); - expectValueFrom('{ requiredBool: $foo }', testInputObj, { + expectValueFrom('{ requiredBool: $foo }', testInputObj, undefined, { foo: true, }).to.deep.equal({ int: 42, diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts index 1a880449c8..74275b63a5 100644 --- a/src/utilities/astFromValue.ts +++ b/src/utilities/astFromValue.ts @@ -38,12 +38,13 @@ import { GraphQLID } from '../type/scalars'; * | null | NullValue | * */ -export function astFromValue( +export function astFromValue( value: unknown, type: GraphQLInputType, + context?: Maybe, ): Maybe { if (isNonNullType(type)) { - const astValue = astFromValue(value, type.ofType); + const astValue = astFromValue(value, type.ofType, context); if (astValue?.kind === Kind.NULL) { return null; } @@ -67,7 +68,7 @@ export function astFromValue( if (isIterableObject(value)) { const valuesNodes = []; for (const item of value) { - const itemNode = astFromValue(item, itemType); + const itemNode = astFromValue(item, itemType, context); if (itemNode != null) { valuesNodes.push(itemNode); } @@ -85,7 +86,7 @@ export function astFromValue( } const fieldNodes: Array = []; for (const field of Object.values(type.getFields())) { - const fieldValue = astFromValue(value[field.name], field.type); + const fieldValue = astFromValue(value[field.name], field.type, context); if (fieldValue) { fieldNodes.push({ kind: Kind.OBJECT_FIELD, @@ -100,7 +101,7 @@ export function astFromValue( if (isLeafType(type)) { // Since value is an internally represented value, it must be serialized // to an externally represented value before converting into an AST. - const serialized = type.serialize(value); + const serialized = type.serialize(value, context); if (serialized == null) { return null; } diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index 136bee63c9..cb57b50f2c 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -27,12 +27,13 @@ type OnErrorCB = ( /** * Coerces a JavaScript value given a GraphQL Input Type. */ -export function coerceInputValue( +export function coerceInputValue( inputValue: unknown, type: GraphQLInputType, + context: TContext, onError: OnErrorCB = defaultOnError, ): unknown { - return coerceInputValueImpl(inputValue, type, onError, undefined); + return coerceInputValueImpl(inputValue, type, context, onError, undefined); } function defaultOnError( @@ -48,15 +49,22 @@ function defaultOnError( throw error; } -function coerceInputValueImpl( +function coerceInputValueImpl( inputValue: unknown, type: GraphQLInputType, + context: TContext, onError: OnErrorCB, path: Path | undefined, ): unknown { if (isNonNullType(type)) { if (inputValue != null) { - return coerceInputValueImpl(inputValue, type.ofType, onError, path); + return coerceInputValueImpl( + inputValue, + type.ofType, + context, + onError, + path, + ); } onError( pathToArray(path), @@ -78,11 +86,17 @@ function coerceInputValueImpl( if (isIterableObject(inputValue)) { return Array.from(inputValue, (itemValue, index) => { const itemPath = addPath(path, index, undefined); - return coerceInputValueImpl(itemValue, itemType, onError, itemPath); + return coerceInputValueImpl( + itemValue, + itemType, + context, + onError, + itemPath, + ); }); } // Lists accept a non-list value as a list of one. - return [coerceInputValueImpl(inputValue, itemType, onError, path)]; + return [coerceInputValueImpl(inputValue, itemType, context, onError, path)]; } if (isInputObjectType(type)) { @@ -120,6 +134,7 @@ function coerceInputValueImpl( coercedValue[field.name] = coerceInputValueImpl( fieldValue, field.type, + context, onError, addPath(path, field.name, type.name), ); @@ -152,7 +167,7 @@ function coerceInputValueImpl( // which can throw to indicate failure. If it throws, maintain a reference // to the original error. try { - parseResult = type.parseValue(inputValue); + parseResult = type.parseValue(inputValue, context); } catch (error) { if (error instanceof GraphQLError) { onError(pathToArray(path), inputValue, error); diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts index 4f0cee6b29..7bb55f9e0b 100644 --- a/src/utilities/valueFromAST.ts +++ b/src/utilities/valueFromAST.ts @@ -35,9 +35,10 @@ import { * | NullValue | null | * */ -export function valueFromAST( +export function valueFromAST( valueNode: Maybe, type: GraphQLInputType, + context?: Maybe, variables?: Maybe>, ): unknown { if (!valueNode) { @@ -66,7 +67,7 @@ export function valueFromAST( if (valueNode.kind === Kind.NULL) { return; // Invalid: intentionally return no value. } - return valueFromAST(valueNode, type.ofType, variables); + return valueFromAST(valueNode, type.ofType, context, variables); } if (valueNode.kind === Kind.NULL) { @@ -87,7 +88,12 @@ export function valueFromAST( } coercedValues.push(null); } else { - const itemValue = valueFromAST(itemNode, itemType, variables); + const itemValue = valueFromAST( + itemNode, + itemType, + context, + variables, + ); if (itemValue === undefined) { return; // Invalid: intentionally return no value. } @@ -96,7 +102,7 @@ export function valueFromAST( } return coercedValues; } - const coercedValue = valueFromAST(valueNode, itemType, variables); + const coercedValue = valueFromAST(valueNode, itemType, context, variables); if (coercedValue === undefined) { return; // Invalid: intentionally return no value. } @@ -119,7 +125,12 @@ export function valueFromAST( } continue; } - const fieldValue = valueFromAST(fieldNode.value, field.type, variables); + const fieldValue = valueFromAST( + fieldNode.value, + field.type, + context, + variables, + ); if (fieldValue === undefined) { return; // Invalid: intentionally return no value. } @@ -134,7 +145,7 @@ export function valueFromAST( // no value is returned. let result; try { - result = type.parseLiteral(valueNode, variables); + result = type.parseLiteral(valueNode, context, variables); } catch (_error) { return; // Invalid: intentionally return no value. } diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.ts b/src/validation/rules/ValuesOfCorrectTypeRule.ts index 5d81a3833a..e07f2b6332 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.ts +++ b/src/validation/rules/ValuesOfCorrectTypeRule.ts @@ -126,7 +126,11 @@ function isValidValueNode(context: ValidationContext, node: ValueNode): void { // Scalars and Enums determine if a literal value is valid via parseLiteral(), // which may throw or return an invalid value to indicate failure. try { - const parseResult = type.parseLiteral(node, undefined /* variables */); + const parseResult = type.parseLiteral( + node, + undefined /* context */, + undefined /* variables */, + ); if (parseResult === undefined) { const typeStr = inspect(locationType); context.reportError(