Skip to content

Commit 928a321

Browse files
committed
let GraphQLArgs inherit all parse/validate/execute options (#4561)
motivation: - reduces maintenance burden - increases utility/flexibility of the `graphql` pipeline function potential downside: - means that we have to police any ambiguity or clashes regarding option property names between `ParseOptions`, `ValidationOptions`, and `ExecutionArgs`. This will hopefully be mitigated in the future in the context of an overall option "cleanup" where we: (a) differentiate request/spec defined "options/args" and implementation-specific options (b) further group options, especially with regard to our growing list of execution options and (c) potentially introduce a more standardized namespace/convention for experimental-options.
1 parent 0ad48ea commit 928a321

File tree

3 files changed

+183
-45
lines changed

3 files changed

+183
-45
lines changed

src/__tests__/graphql-test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { GraphQLError } from '../error/GraphQLError.js';
5+
6+
import { Source } from '../language/source.js';
7+
8+
import { GraphQLObjectType } from '../type/definition.js';
9+
import { GraphQLString } from '../type/scalars.js';
10+
import { GraphQLSchema } from '../type/schema.js';
11+
12+
import type { ValidationRule } from '../validation/ValidationContext.js';
13+
14+
import { graphql, graphqlSync } from '../graphql.js';
15+
16+
const schema = new GraphQLSchema({
17+
query: new GraphQLObjectType({
18+
name: 'Query',
19+
fields: {
20+
a: {
21+
type: GraphQLString,
22+
resolve: () => 'A',
23+
},
24+
b: {
25+
type: GraphQLString,
26+
resolve: () => 'B',
27+
},
28+
contextEcho: {
29+
type: GraphQLString,
30+
resolve: (_source, _args, contextValue) => String(contextValue),
31+
},
32+
syncField: {
33+
type: GraphQLString,
34+
resolve: (rootValue) => rootValue,
35+
},
36+
asyncField: {
37+
type: GraphQLString,
38+
resolve: (rootValue) => Promise.resolve(rootValue),
39+
},
40+
},
41+
}),
42+
});
43+
44+
describe('graphql', () => {
45+
it('passes source through to parse', async () => {
46+
const source = new Source('{', 'custom-query.graphql');
47+
48+
const result = await graphql({ schema, source });
49+
50+
expect(result.errors?.[0]?.source?.name).to.equal('custom-query.graphql');
51+
});
52+
53+
it('passes rules through to validate', async () => {
54+
const customRule: ValidationRule = (context) => ({
55+
Field(node) {
56+
context.reportError(
57+
new GraphQLError('custom rule error', {
58+
nodes: node,
59+
}),
60+
);
61+
},
62+
});
63+
64+
const result = await graphql({
65+
schema,
66+
source: '{ a }',
67+
rules: [customRule],
68+
});
69+
70+
expect(result.errors?.[0]?.message).to.equal('custom rule error');
71+
});
72+
73+
it('passes parse options through to parse', async () => {
74+
const customRule: ValidationRule = (context) => ({
75+
OperationDefinition(node) {
76+
context.reportError(
77+
new GraphQLError(
78+
node.loc === undefined ? 'no location' : 'has location',
79+
{
80+
nodes: node,
81+
},
82+
),
83+
);
84+
},
85+
});
86+
87+
const result = await graphql({
88+
schema,
89+
source: '{ a }',
90+
noLocation: true,
91+
rules: [customRule],
92+
});
93+
94+
expect(result.errors?.[0]?.message).to.equal('no location');
95+
});
96+
97+
it('passes validation options through to validate', async () => {
98+
const result = await graphql({
99+
schema,
100+
source: '{ contextEho }',
101+
hideSuggestions: true,
102+
});
103+
104+
expect(result.errors?.[0]?.message).to.equal(
105+
'Cannot query field "contextEho" on type "Query".',
106+
);
107+
});
108+
109+
it('passes execution args through to execute', async () => {
110+
const result = await graphql({
111+
schema,
112+
source: `
113+
query First {
114+
a
115+
}
116+
117+
query Second {
118+
b
119+
}
120+
`,
121+
operationName: 'Second',
122+
});
123+
124+
expect(result).to.deep.equal({
125+
data: {
126+
b: 'B',
127+
},
128+
});
129+
});
130+
131+
it('returns schema validation errors', async () => {
132+
const badSchema = new GraphQLSchema({});
133+
const result = await graphql({
134+
schema: badSchema,
135+
source: '{ __typename }',
136+
});
137+
138+
expect(result.errors?.[0]?.message).to.equal(
139+
'Query root type must be provided.',
140+
);
141+
});
142+
});
143+
144+
describe('graphqlSync', () => {
145+
it('returns result for synchronous execution', () => {
146+
const result = graphqlSync({
147+
schema,
148+
source: '{ syncField }',
149+
rootValue: 'rootValue',
150+
});
151+
152+
expect(result).to.deep.equal({ data: { syncField: 'rootValue' } });
153+
});
154+
155+
it('throws for asynchronous execution', () => {
156+
expect(() => {
157+
graphqlSync({
158+
schema,
159+
source: '{ asyncField }',
160+
rootValue: 'rootValue',
161+
});
162+
}).to.throw('GraphQL execution failed to complete synchronously.');
163+
});
164+
});

src/graphql.ts

Lines changed: 13 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
import { isPromise } from './jsutils/isPromise.js';
2-
import type { Maybe } from './jsutils/Maybe.js';
32
import type { PromiseOrValue } from './jsutils/PromiseOrValue.js';
43

4+
import type { ParseOptions } from './language/parser.js';
55
import { parse } from './language/parser.js';
66
import type { Source } from './language/source.js';
77

8-
import type {
9-
GraphQLFieldResolver,
10-
GraphQLTypeResolver,
11-
} from './type/definition.js';
12-
import type { GraphQLSchema } from './type/schema.js';
138
import { validateSchema } from './type/validate.js';
149

10+
import type { ValidationOptions } from './validation/validate.js';
1511
import { validate } from './validation/validate.js';
12+
import type { ValidationRule } from './validation/ValidationContext.js';
1613

14+
import type { ExecutionArgs } from './execution/execute.js';
1715
import { execute } from './execution/execute.js';
1816
import type { ExecutionResult } from './execution/Executor.js';
1917

@@ -56,17 +54,12 @@ import type { ExecutionResult } from './execution/Executor.js';
5654
* If not provided, the default type resolver is used (which looks for a
5755
* `__typename` field or alternatively calls the `isTypeOf` method).
5856
*/
59-
export interface GraphQLArgs {
60-
schema: GraphQLSchema;
57+
export interface GraphQLArgs
58+
extends ParseOptions,
59+
ValidationOptions,
60+
Omit<ExecutionArgs, 'document'> {
6161
source: string | Source;
62-
hideSuggestions?: Maybe<boolean>;
63-
rootValue?: unknown;
64-
contextValue?: unknown;
65-
variableValues?: Maybe<{ readonly [variable: string]: unknown }>;
66-
operationName?: Maybe<string>;
67-
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
68-
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
69-
abortSignal?: Maybe<AbortSignal>;
62+
rules?: ReadonlyArray<ValidationRule> | undefined;
7063
}
7164

7265
export function graphql(args: GraphQLArgs): Promise<ExecutionResult> {
@@ -92,18 +85,7 @@ export function graphqlSync(args: GraphQLArgs): ExecutionResult {
9285
}
9386

9487
function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
95-
const {
96-
schema,
97-
source,
98-
rootValue,
99-
contextValue,
100-
variableValues,
101-
operationName,
102-
fieldResolver,
103-
typeResolver,
104-
hideSuggestions,
105-
abortSignal,
106-
} = args;
88+
const { schema, source } = args;
10789

10890
// Validate Schema
10991
const schemaValidationErrors = validateSchema(schema);
@@ -114,30 +96,17 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
11496
// Parse
11597
let document;
11698
try {
117-
document = parse(source);
99+
document = parse(source, args);
118100
} catch (syntaxError) {
119101
return { errors: [syntaxError] };
120102
}
121103

122104
// Validate
123-
const validationErrors = validate(schema, document, undefined, {
124-
hideSuggestions,
125-
});
105+
const validationErrors = validate(schema, document, args.rules, args);
126106
if (validationErrors.length > 0) {
127107
return { errors: validationErrors };
128108
}
129109

130110
// Execute
131-
return execute({
132-
schema,
133-
document,
134-
rootValue,
135-
contextValue,
136-
variableValues,
137-
operationName,
138-
fieldResolver,
139-
typeResolver,
140-
hideSuggestions,
141-
abortSignal,
142-
});
111+
return execute({ ...args, document });
143112
}

src/validation/validate.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import {
1919
ValidationContext,
2020
} from './ValidationContext.js';
2121

22+
export interface ValidationOptions {
23+
maxErrors?: number;
24+
hideSuggestions?: Maybe<boolean>;
25+
}
26+
2227
// Per the specification, descriptions must not affect validation.
2328
// See https://spec.graphql.org/draft/#sec-Descriptions
2429
const QueryDocumentKeysToValidate = mapValue(
@@ -50,7 +55,7 @@ export function validate(
5055
schema: GraphQLSchema,
5156
documentAST: DocumentNode,
5257
rules: ReadonlyArray<ValidationRule> = specifiedRules,
53-
options?: { maxErrors?: number; hideSuggestions?: Maybe<boolean> },
58+
options?: ValidationOptions,
5459
): ReadonlyArray<GraphQLError> {
5560
const maxErrors = options?.maxErrors ?? 100;
5661
const hideSuggestions = options?.hideSuggestions ?? false;

0 commit comments

Comments
 (0)