Skip to content

Allow scalar parse* and serialized methods to access context #3600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/execution/__tests__/executor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand Down
23 changes: 22 additions & 1 deletion src/execution/__tests__/variables-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,19 @@ const TestComplexScalar = new GraphQLScalarType({
},
});

const TestContextScalar = new GraphQLScalarType({
name: 'ContextScalar',
parseValue: (_value, context) => context,
});

const TestInputObject = new GraphQLInputObjectType({
name: 'TestInputObject',
fields: {
a: { type: GraphQLString },
b: { type: new GraphQLList(GraphQLString) },
c: { type: new GraphQLNonNull(GraphQLString) },
d: { type: TestComplexScalar },
e: { type: TestContextScalar },
},
});

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1044,6 +1063,7 @@ describe('Execute: Handles inputs', () => {
schema,
variableDefinitions,
inputValue,
undefined,
{ maxErrors: 3 },
);

Expand All @@ -1061,6 +1081,7 @@ describe('Execute: Handles inputs', () => {
schema,
variableDefinitions,
inputValue,
undefined,
{ maxErrors: 2 },
);

Expand Down
22 changes: 14 additions & 8 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ export function buildExecutionContext(
schema,
variableDefinitions,
rawVariableValues ?? {},
contextValue,
{ maxErrors: 50 },
);

Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ` +
Expand Down
25 changes: 17 additions & 8 deletions src/execution/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,17 @@ export async function createSourceEventStream(
async function executeSubscription(
exeContext: ExecutionContext,
): Promise<unknown> {
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) {
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 14 additions & 4 deletions src/execution/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,19 @@ type CoercedVariableValues =
* exposed to user code. Care should be taken to not pull values from the
* Object prototype.
*/
export function getVariableValues(
export function getVariableValues<TContext = any>(
schema: GraphQLSchema,
varDefNodes: ReadonlyArray<VariableDefinitionNode>,
inputs: { readonly [variable: string]: unknown },
context?: Maybe<TContext>,
options?: { maxErrors?: number },
): CoercedVariableValues {
const errors = [];
const maxErrors = options?.maxErrors;
try {
const coerced = coerceVariableValues(
schema,
context,
varDefNodes,
inputs,
(error) => {
Expand All @@ -69,8 +71,9 @@ export function getVariableValues(
return { errors };
}

function coerceVariableValues(
function coerceVariableValues<TContext>(
schema: GraphQLSchema,
context: TContext,
varDefNodes: ReadonlyArray<VariableDefinitionNode>,
inputs: { readonly [variable: string]: unknown },
onError: (error: GraphQLError) => void,
Expand Down Expand Up @@ -122,6 +125,7 @@ function coerceVariableValues(
coercedValues[varName] = coerceInputValue(
value,
varType,
context,
(path, invalidValue, error) => {
let prefix =
`Variable "$${varName}" got invalid value ` + inspect(invalidValue);
Expand Down Expand Up @@ -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<TContext = any>(
def: GraphQLField<unknown, unknown> | GraphQLDirective,
node: FieldNode | DirectiveNode,
context?: TContext,
variableValues?: Maybe<ObjMap<unknown>>,
): { [argument: string]: unknown } {
const coercedValues: { [argument: string]: unknown } = {};
Expand Down Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion src/type/__tests__/definition-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
Loading