Skip to content

Commit 735f0e4

Browse files
yaacovCRn1ru4l
andauthored
add graphql "harness" abstraction along with async parse/validate support (graphql#4562)
This PR extends the functionality of the graphql function by allowing users to pass in a custom harness with user-supplied parse/validate/execute/subscribe functions. This allows users to pass custom versions of those functions, enabling a simple API for adding pre/post hooks, a very simplified version of the pattern introduced by [Envelop](https://the-guild.dev/graphql/envelop). Although this extends what is possible with the graphql function, which is neat, the underlying purpose is not to compete with Envelop and other frameworks, but rather to facilitate them, background at graphql#3421. The introduction of the `GraphQLParseFn`, `GraphQLValidateFn`, `GraphQLExecuteFn` and `GraphQLSubscribeFn` types for the functions which make up the harness include purposeful maybe-async return types, even though our internal `parse` and `validate` functions are always sync, to encourage servers and other tooling to expect that user-supplied versions of these functions may have async pre/post hooks. This is a softened response to the request by Envelop maintainers in graphql#3421 to wrap the `parse` result in a promise. This PR nudges servers and other tooling in that direction by exposing types that return maybe-async results, but is a non-breaking change. Co-authored-by: Laurin Quast <laurinquast@googlemail.com>
1 parent 8866752 commit 735f0e4

File tree

4 files changed

+205
-8
lines changed

4 files changed

+205
-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 GraphQLValidateFn = (
14+
...args: Parameters<typeof validate>
15+
) => PromiseOrValue<ReturnType<typeof validate>>;
16+
17+
export type GraphQLExecuteFn = (
18+
...args: Parameters<typeof execute>
19+
) => ReturnType<typeof execute>;
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+
};

src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ export { enableDevMode, isDevModeEnabled } from './devMode.js';
3636
export type { GraphQLArgs } from './graphql.js';
3737
export { graphql, graphqlSync } from './graphql.js';
3838

39+
// The default versions of the parse/validate/execute/subscribe harness used by `graphql` and `graphqlSync`.
40+
export { defaultHarness } from './harness.js';
41+
export type {
42+
GraphQLHarness,
43+
GraphQLParseFn,
44+
GraphQLValidateFn,
45+
GraphQLExecuteFn,
46+
GraphQLSubscribeFn,
47+
} from './harness.js';
48+
3949
// Create and operate on GraphQL type definitions and schema.
4050
export type {
4151
GraphQLField,

0 commit comments

Comments
 (0)