Skip to content

Commit 260ad30

Browse files
committed
add graphql "harness" abstraction along with async parse/validate support
see graphql#3421
1 parent 16fccd0 commit 260ad30

File tree

3 files changed

+195
-8
lines changed

3 files changed

+195
-8
lines changed

src/__tests__/graphql-test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import { GraphQLSchema } from '../type/schema.js';
1111

1212
import type { ValidationRule } from '../validation/ValidationContext.js';
1313

14+
import { execute } from '../execution/execute.js';
15+
1416
import { graphql, graphqlSync } from '../graphql.js';
17+
import { defaultHarness } from '../harness.js';
1518

1619
const schema = new GraphQLSchema({
1720
query: new GraphQLObjectType({
@@ -139,6 +142,109 @@ describe('graphql', () => {
139142
'Query root type must be provided.',
140143
);
141144
});
145+
146+
it('works when a custom harness is provided', async () => {
147+
const result = await graphql({
148+
schema,
149+
source: '{ syncField }',
150+
rootValue: 'rootValue',
151+
harness: {
152+
...defaultHarness,
153+
execute: (args) =>
154+
execute({ ...args, rootValue: `**${args.rootValue}**` }),
155+
},
156+
});
157+
158+
expect(result).to.deep.equal({ data: { syncField: '**rootValue**' } });
159+
});
160+
161+
it('returns parse errors thrown synchronously by a custom harness', async () => {
162+
const parseError = new GraphQLError('sync parse error');
163+
const result = await graphql({
164+
schema,
165+
source: '{ syncField }',
166+
harness: {
167+
...defaultHarness,
168+
parse: () => {
169+
throw parseError;
170+
},
171+
},
172+
});
173+
174+
expect(result).to.deep.equal({ errors: [parseError] });
175+
});
176+
177+
it('works with asynchronous parse from a custom harness', async () => {
178+
const result = await graphql({
179+
schema,
180+
source: '{ syncField }',
181+
rootValue: 'rootValue',
182+
harness: {
183+
...defaultHarness,
184+
parse: (source, options) =>
185+
Promise.resolve(defaultHarness.parse(source, options)),
186+
},
187+
});
188+
189+
expect(result).to.deep.equal({ data: { syncField: 'rootValue' } });
190+
});
191+
192+
it('handles errors from an asynchronous parse from a custom harness', async () => {
193+
const parseError = new GraphQLError('async parse error');
194+
const result = await graphql({
195+
schema,
196+
source: '{ syncField }',
197+
harness: {
198+
...defaultHarness,
199+
parse: () => Promise.reject(parseError),
200+
},
201+
});
202+
203+
expect(result).to.deep.equal({ errors: [parseError] });
204+
});
205+
206+
it('works with asynchronous validation from a custom harness', async () => {
207+
const result = await graphql({
208+
schema,
209+
source: '{ syncField }',
210+
rootValue: 'rootValue',
211+
harness: {
212+
...defaultHarness,
213+
validate: (s, document) =>
214+
Promise.resolve(defaultHarness.validate(s, document)),
215+
},
216+
});
217+
218+
expect(result).to.deep.equal({ data: { syncField: 'rootValue' } });
219+
});
220+
221+
it('returns validation errors from synchronous validation from a custom harness', async () => {
222+
const validationError = new GraphQLError('async validation error');
223+
const result = await graphql({
224+
schema,
225+
source: '{ syncField }',
226+
harness: {
227+
...defaultHarness,
228+
validate: () => [validationError],
229+
},
230+
});
231+
232+
expect(result).to.deep.equal({ errors: [validationError] });
233+
});
234+
235+
it('returns validation errors from asynchronous validation from a custom harness', async () => {
236+
const validationError = new GraphQLError('async validation error');
237+
const result = await graphql({
238+
schema,
239+
source: '{ syncField }',
240+
harness: {
241+
...defaultHarness,
242+
validate: () => Promise.resolve([validationError]),
243+
},
244+
});
245+
246+
expect(result).to.deep.equal({ errors: [validationError] });
247+
});
142248
});
143249

144250
describe('graphqlSync', () => {

src/graphql.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import { isPromise } from './jsutils/isPromise.js';
22
import type { PromiseOrValue } from './jsutils/PromiseOrValue.js';
33

4+
import type { GraphQLError } from './error/GraphQLError.js';
5+
6+
import type { DocumentNode } from './language/ast.js';
47
import type { ParseOptions } from './language/parser.js';
5-
import { parse } from './language/parser.js';
68
import type { Source } from './language/source.js';
79

10+
import type { GraphQLSchema } from './type/schema.js';
811
import { validateSchema } from './type/validate.js';
912

1013
import type { ValidationOptions } from './validation/validate.js';
11-
import { validate } from './validation/validate.js';
1214
import type { ValidationRule } from './validation/ValidationContext.js';
1315

1416
import type { ExecutionArgs } from './execution/execute.js';
15-
import { execute } from './execution/execute.js';
1617
import type { ExecutionResult } from './execution/Executor.js';
1718

19+
import type { GraphQLHarness } from './harness.js';
20+
import { defaultHarness } from './harness.js';
21+
1822
/**
1923
* This is the primary entry point function for fulfilling GraphQL operations
2024
* by parsing, validating, and executing a GraphQL document along side a
@@ -60,6 +64,7 @@ export interface GraphQLArgs
6064
extends ParseOptions,
6165
ValidationOptions,
6266
Omit<ExecutionArgs, 'document'> {
67+
harness?: GraphQLHarness | undefined;
6368
source: string | Source;
6469
rules?: ReadonlyArray<ValidationRule> | undefined;
6570
}
@@ -87,6 +92,7 @@ export function graphqlSync(args: GraphQLArgs): ExecutionResult {
8792
}
8893

8994
function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
95+
const harness = args.harness ?? defaultHarness;
9096
const { schema, source } = args;
9197

9298
// Validate Schema
@@ -98,17 +104,55 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
98104
// Parse
99105
let document;
100106
try {
101-
document = parse(source, args);
107+
document = harness.parse(source, args);
102108
} catch (syntaxError) {
103109
return { errors: [syntaxError] };
104110
}
105111

112+
if (isPromise(document)) {
113+
return document.then(
114+
(resolvedDocument) =>
115+
validateAndExecute(harness, args, schema, resolvedDocument),
116+
(syntaxError: unknown) => ({ errors: [syntaxError as GraphQLError] }),
117+
);
118+
}
119+
120+
return validateAndExecute(harness, args, schema, document);
121+
}
122+
123+
function validateAndExecute(
124+
harness: GraphQLHarness,
125+
args: GraphQLArgs,
126+
schema: GraphQLSchema,
127+
document: DocumentNode,
128+
): PromiseOrValue<ExecutionResult> {
106129
// Validate
107-
const validationErrors = validate(schema, document, args.rules, args);
108-
if (validationErrors.length > 0) {
109-
return { errors: validationErrors };
130+
const validationResult = harness.validate(schema, document, args.rules, args);
131+
132+
if (isPromise(validationResult)) {
133+
return validationResult.then((resolvedValidationResult) =>
134+
checkValidationAndExecute(
135+
harness,
136+
args,
137+
resolvedValidationResult,
138+
document,
139+
),
140+
);
141+
}
142+
143+
return checkValidationAndExecute(harness, args, validationResult, document);
144+
}
145+
146+
function checkValidationAndExecute(
147+
harness: GraphQLHarness,
148+
args: GraphQLArgs,
149+
validationResult: ReadonlyArray<GraphQLError>,
150+
document: DocumentNode,
151+
): PromiseOrValue<ExecutionResult> {
152+
if (validationResult.length > 0) {
153+
return { errors: validationResult };
110154
}
111155

112156
// Execute
113-
return execute({ ...args, document });
157+
return harness.execute({ ...args, document });
114158
}

src/harness.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { PromiseOrValue } from './jsutils/PromiseOrValue.js';
2+
3+
import { parse } from './language/parser.js';
4+
5+
import { validate } from './validation/validate.js';
6+
7+
import { execute, subscribe } from './execution/execute.js';
8+
9+
export type GraphQLParseFn = (
10+
...args: Parameters<typeof parse>
11+
) => PromiseOrValue<ReturnType<typeof parse>>;
12+
13+
export type GraphQLExecuteFn = (
14+
...args: Parameters<typeof execute>
15+
) => ReturnType<typeof execute>;
16+
17+
export type GraphQLValidateFn = (
18+
...args: Parameters<typeof validate>
19+
) => PromiseOrValue<ReturnType<typeof validate>>;
20+
21+
export type GraphQLSubscribeFn = (
22+
...args: Parameters<typeof subscribe>
23+
) => ReturnType<typeof subscribe>;
24+
25+
export interface GraphQLHarness {
26+
parse: GraphQLParseFn;
27+
validate: GraphQLValidateFn;
28+
execute: GraphQLExecuteFn;
29+
subscribe: GraphQLSubscribeFn;
30+
}
31+
32+
export const defaultHarness: GraphQLHarness = {
33+
parse,
34+
validate,
35+
execute,
36+
subscribe,
37+
};

0 commit comments

Comments
 (0)