From fe2a096fb8e06757f8feea52b2fe104d525300f9 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 20 May 2022 08:56:40 +0300 Subject: [PATCH] Allow scalar `parse*` and `serialized` methods to access context Even when provided, `context` must be optional because: 1. Default values for scalar are parsed when building a schema, outside of the execution context. 2. Default values for scalar are serialized during findBreakingChanges and other schema operations, also be outside of the execution context. BREAKING CHANGES are included for argument lists of several functions to include the optional context parameter: 1. `getVariableValues` 2. `getArgumentValues` 3. `coerceInputValue` 4. `valueFromAST` 5. `astFromValue` --- src/execution/__tests__/executor-test.ts | 88 +++++++++++++++++++ src/execution/__tests__/variables-test.ts | 23 ++++- src/execution/execute.ts | 22 +++-- src/execution/subscribe.ts | 25 ++++-- src/execution/values.ts | 18 +++- src/type/__tests__/definition-test.ts | 17 +++- src/type/__tests__/introspection-test.ts | 16 +++- src/type/definition.ts | 25 ++++-- src/type/introspection.ts | 4 +- src/utilities/__tests__/astFromValue-test.ts | 12 +++ .../__tests__/coerceInputValue-test.ts | 26 +++++- src/utilities/__tests__/valueFromAST-test.ts | 54 ++++++++---- src/utilities/astFromValue.ts | 11 +-- src/utilities/coerceInputValue.ts | 29 ++++-- src/utilities/valueFromAST.ts | 23 +++-- .../rules/ValuesOfCorrectTypeRule.ts | 6 +- 16 files changed, 330 insertions(+), 69 deletions(-) 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(