Skip to content

Commit 0694bab

Browse files
committed
addFieldError can return the errors array as a convenience
1 parent f4b118e commit 0694bab

File tree

10 files changed

+216
-223
lines changed

10 files changed

+216
-223
lines changed

src/execution/IncrementalPublisher.ts

Lines changed: 187 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,72 @@
1+
import { isPromise } from '../jsutils/isPromise.js';
12
import type { ObjMap } from '../jsutils/ObjMap.js';
23
import type { Path } from '../jsutils/Path.js';
34
import { pathToArray } from '../jsutils/Path.js';
5+
import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js';
46
import { promiseWithResolvers } from '../jsutils/promiseWithResolvers.js';
57

68
import type {
79
GraphQLError,
810
GraphQLFormattedError,
911
} from '../error/GraphQLError.js';
1012

13+
/**
14+
* The result of GraphQL execution.
15+
*
16+
* - `errors` is included when any errors occurred as a non-empty array.
17+
* - `data` is the result of a successful execution of the query.
18+
* - `hasNext` is true if a future payload is expected.
19+
* - `extensions` is reserved for adding non-standard properties.
20+
* - `incremental` is a list of the results from defer/stream directives.
21+
*/
22+
export interface ExecutionResult<
23+
TData = ObjMap<unknown>,
24+
TExtensions = ObjMap<unknown>,
25+
> {
26+
errors?: ReadonlyArray<GraphQLError>;
27+
data?: TData | null;
28+
extensions?: TExtensions;
29+
}
30+
31+
export interface FormattedExecutionResult<
32+
TData = ObjMap<unknown>,
33+
TExtensions = ObjMap<unknown>,
34+
> {
35+
errors?: ReadonlyArray<GraphQLFormattedError>;
36+
data?: TData | null;
37+
extensions?: TExtensions;
38+
}
39+
40+
export interface ExperimentalIncrementalExecutionResults<
41+
TData = ObjMap<unknown>,
42+
TExtensions = ObjMap<unknown>,
43+
> {
44+
initialResult: InitialIncrementalExecutionResult<TData, TExtensions>;
45+
subsequentResults: AsyncGenerator<
46+
SubsequentIncrementalExecutionResult<TData, TExtensions>,
47+
void,
48+
void
49+
>;
50+
}
51+
52+
export interface InitialIncrementalExecutionResult<
53+
TData = ObjMap<unknown>,
54+
TExtensions = ObjMap<unknown>,
55+
> extends ExecutionResult<TData, TExtensions> {
56+
hasNext: boolean;
57+
incremental?: ReadonlyArray<IncrementalResult<TData, TExtensions>>;
58+
extensions?: TExtensions;
59+
}
60+
61+
export interface FormattedInitialIncrementalExecutionResult<
62+
TData = ObjMap<unknown>,
63+
TExtensions = ObjMap<unknown>,
64+
> extends FormattedExecutionResult<TData, TExtensions> {
65+
hasNext: boolean;
66+
incremental?: ReadonlyArray<FormattedIncrementalResult<TData, TExtensions>>;
67+
extensions?: TExtensions;
68+
}
69+
1170
export interface SubsequentIncrementalExecutionResult<
1271
TData = ObjMap<unknown>,
1372
TExtensions = ObjMap<unknown>,
@@ -113,86 +172,6 @@ export class IncrementalPublisher {
113172
this._reset();
114173
}
115174

116-
hasNext(): boolean {
117-
return this._pending.size > 0;
118-
}
119-
120-
subscribe(): AsyncGenerator<
121-
SubsequentIncrementalExecutionResult,
122-
void,
123-
void
124-
> {
125-
let isDone = false;
126-
127-
const _next = async (): Promise<
128-
IteratorResult<SubsequentIncrementalExecutionResult, void>
129-
> => {
130-
// eslint-disable-next-line no-constant-condition
131-
while (true) {
132-
if (isDone) {
133-
return { value: undefined, done: true };
134-
}
135-
136-
for (const item of this._released) {
137-
this._pending.delete(item);
138-
}
139-
const released = this._released;
140-
this._released = new Set();
141-
142-
const result = this._getIncrementalResult(released);
143-
144-
if (!this.hasNext()) {
145-
isDone = true;
146-
}
147-
148-
if (result !== undefined) {
149-
return { value: result, done: false };
150-
}
151-
152-
// eslint-disable-next-line no-await-in-loop
153-
await this._signalled;
154-
}
155-
};
156-
157-
const returnStreamIterators = async (): Promise<void> => {
158-
const promises: Array<Promise<IteratorResult<unknown>>> = [];
159-
this._pending.forEach((incrementalDataRecord) => {
160-
if (
161-
isStreamItemsRecord(incrementalDataRecord) &&
162-
incrementalDataRecord.asyncIterator?.return
163-
) {
164-
promises.push(incrementalDataRecord.asyncIterator.return());
165-
}
166-
});
167-
await Promise.all(promises);
168-
};
169-
170-
const _return = async (): Promise<
171-
IteratorResult<SubsequentIncrementalExecutionResult, void>
172-
> => {
173-
isDone = true;
174-
await returnStreamIterators();
175-
return { value: undefined, done: true };
176-
};
177-
178-
const _throw = async (
179-
error?: unknown,
180-
): Promise<IteratorResult<SubsequentIncrementalExecutionResult, void>> => {
181-
isDone = true;
182-
await returnStreamIterators();
183-
return Promise.reject(error);
184-
};
185-
186-
return {
187-
[Symbol.asyncIterator]() {
188-
return this;
189-
},
190-
next: _next,
191-
return: _return,
192-
throw: _throw,
193-
};
194-
}
195-
196175
prepareInitialResultRecord(): InitialResultRecord {
197176
return {
198177
errors: [],
@@ -253,23 +232,32 @@ export class IncrementalPublisher {
253232
addFieldError(
254233
incrementalDataRecord: IncrementalDataRecord,
255234
error: GraphQLError,
256-
) {
257-
incrementalDataRecord.errors.push(error);
235+
): ReadonlyArray<GraphQLError> {
236+
const errors = incrementalDataRecord.errors;
237+
errors.push(error);
238+
return errors;
258239
}
259240

260-
publishInitial(initialResult: InitialResultRecord) {
261-
for (const child of initialResult.children) {
262-
if (child.filtered) {
263-
continue;
264-
}
265-
this._publish(child);
241+
handleInitialResultData(
242+
initialResultRecord: InitialResultRecord,
243+
data: PromiseOrValue<ObjMap<unknown>>,
244+
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
245+
if (isPromise(data)) {
246+
return data.then(
247+
(resolved) => this._buildInitialResponse(initialResultRecord, resolved),
248+
(error) => this.handleInitialResultError(initialResultRecord, error),
249+
);
266250
}
251+
return this._buildInitialResponse(initialResultRecord, data);
267252
}
268253

269-
getInitialErrors(
270-
initialResult: InitialResultRecord,
271-
): ReadonlyArray<GraphQLError> {
272-
return initialResult.errors;
254+
handleInitialResultError(
255+
initialResultRecord: InitialResultRecord,
256+
error: GraphQLError,
257+
): ExecutionResult {
258+
const errors = initialResultRecord.errors;
259+
errors.push(error);
260+
return { data: null, errors };
273261
}
274262

275263
filter(nullPath: Path, erroringIncrementalDataRecord: IncrementalDataRecord) {
@@ -303,6 +291,111 @@ export class IncrementalPublisher {
303291
});
304292
}
305293

294+
private _buildInitialResponse(
295+
initialResultRecord: InitialResultRecord,
296+
data: ObjMap<unknown> | null,
297+
): ExecutionResult | ExperimentalIncrementalExecutionResults {
298+
for (const child of initialResultRecord.children) {
299+
if (child.filtered) {
300+
continue;
301+
}
302+
this._publish(child);
303+
}
304+
305+
const errors = initialResultRecord.errors;
306+
const initialResult = errors.length === 0 ? { data } : { errors, data };
307+
if (this._hasNext()) {
308+
return {
309+
initialResult: {
310+
...initialResult,
311+
hasNext: true,
312+
},
313+
subsequentResults: this._subscribe(),
314+
};
315+
}
316+
return initialResult;
317+
}
318+
319+
private _hasNext(): boolean {
320+
return this._pending.size > 0;
321+
}
322+
323+
private _subscribe(): AsyncGenerator<
324+
SubsequentIncrementalExecutionResult,
325+
void,
326+
void
327+
> {
328+
let isDone = false;
329+
330+
const _next = async (): Promise<
331+
IteratorResult<SubsequentIncrementalExecutionResult, void>
332+
> => {
333+
// eslint-disable-next-line no-constant-condition
334+
while (true) {
335+
if (isDone) {
336+
return { value: undefined, done: true };
337+
}
338+
339+
for (const item of this._released) {
340+
this._pending.delete(item);
341+
}
342+
const released = this._released;
343+
this._released = new Set();
344+
345+
const result = this._getIncrementalResult(released);
346+
347+
if (!this._hasNext()) {
348+
isDone = true;
349+
}
350+
351+
if (result !== undefined) {
352+
return { value: result, done: false };
353+
}
354+
355+
// eslint-disable-next-line no-await-in-loop
356+
await this._signalled;
357+
}
358+
};
359+
360+
const returnStreamIterators = async (): Promise<void> => {
361+
const promises: Array<Promise<IteratorResult<unknown>>> = [];
362+
this._pending.forEach((incrementalDataRecord) => {
363+
if (
364+
isStreamItemsRecord(incrementalDataRecord) &&
365+
incrementalDataRecord.asyncIterator?.return
366+
) {
367+
promises.push(incrementalDataRecord.asyncIterator.return());
368+
}
369+
});
370+
await Promise.all(promises);
371+
};
372+
373+
const _return = async (): Promise<
374+
IteratorResult<SubsequentIncrementalExecutionResult, void>
375+
> => {
376+
isDone = true;
377+
await returnStreamIterators();
378+
return { value: undefined, done: true };
379+
};
380+
381+
const _throw = async (
382+
error?: unknown,
383+
): Promise<IteratorResult<SubsequentIncrementalExecutionResult, void>> => {
384+
isDone = true;
385+
await returnStreamIterators();
386+
return Promise.reject(error);
387+
};
388+
389+
return {
390+
[Symbol.asyncIterator]() {
391+
return this;
392+
},
393+
next: _next,
394+
return: _return,
395+
throw: _throw,
396+
};
397+
}
398+
306399
private _trigger() {
307400
this._resolve();
308401
this._reset();
@@ -377,8 +470,8 @@ export class IncrementalPublisher {
377470
}
378471

379472
return incrementalResults.length
380-
? { incremental: incrementalResults, hasNext: this.hasNext() }
381-
: encounteredCompletedAsyncIterator && !this.hasNext()
473+
? { incremental: incrementalResults, hasNext: this._hasNext() }
474+
: encounteredCompletedAsyncIterator && !this._hasNext()
382475
? { hasNext: false }
383476
: undefined;
384477
}

src/execution/__tests__/defer-test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ import {
1616
import { GraphQLID, GraphQLString } from '../../type/scalars.js';
1717
import { GraphQLSchema } from '../../type/schema.js';
1818

19-
import type { InitialIncrementalExecutionResult } from '../execute.js';
2019
import { execute, experimentalExecuteIncrementally } from '../execute.js';
21-
import type { SubsequentIncrementalExecutionResult } from '../IncrementalPublisher.js';
20+
import type {
21+
InitialIncrementalExecutionResult,
22+
SubsequentIncrementalExecutionResult,
23+
} from '../IncrementalPublisher.js';
2224

2325
const friendType = new GraphQLObjectType({
2426
fields: {

src/execution/__tests__/lists-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import { GraphQLSchema } from '../../type/schema.js';
1818

1919
import { buildSchema } from '../../utilities/buildASTSchema.js';
2020

21-
import type { ExecutionResult } from '../execute.js';
2221
import { execute, executeSync } from '../execute.js';
22+
import type { ExecutionResult } from '../IncrementalPublisher.js';
2323

2424
describe('Execute: Accepts any iterable as list value', () => {
2525
function complete(rootValue: unknown) {

src/execution/__tests__/nonnull-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { GraphQLSchema } from '../../type/schema.js';
1313

1414
import { buildSchema } from '../../utilities/buildASTSchema.js';
1515

16-
import type { ExecutionResult } from '../execute.js';
1716
import { execute, executeSync } from '../execute.js';
17+
import type { ExecutionResult } from '../IncrementalPublisher.js';
1818

1919
const syncError = new Error('sync');
2020
const syncNonNullError = new Error('syncNonNull');

src/execution/__tests__/oneof-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { parse } from '../../language/parser.js';
66

77
import { buildSchema } from '../../utilities/buildASTSchema.js';
88

9-
import type { ExecutionResult } from '../execute.js';
109
import { execute } from '../execute.js';
10+
import type { ExecutionResult } from '../IncrementalPublisher.js';
1111

1212
const schema = buildSchema(`
1313
type Query {

src/execution/__tests__/stream-test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import {
1717
import { GraphQLID, GraphQLString } from '../../type/scalars.js';
1818
import { GraphQLSchema } from '../../type/schema.js';
1919

20-
import type { InitialIncrementalExecutionResult } from '../execute.js';
2120
import { experimentalExecuteIncrementally } from '../execute.js';
22-
import type { SubsequentIncrementalExecutionResult } from '../IncrementalPublisher.js';
21+
import type {
22+
InitialIncrementalExecutionResult,
23+
SubsequentIncrementalExecutionResult,
24+
} from '../IncrementalPublisher.js';
2325

2426
const friendType = new GraphQLObjectType({
2527
fields: {

src/execution/__tests__/subscribe-test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import {
2020
} from '../../type/scalars.js';
2121
import { GraphQLSchema } from '../../type/schema.js';
2222

23-
import type { ExecutionArgs, ExecutionResult } from '../execute.js';
23+
import type { ExecutionArgs } from '../execute.js';
2424
import { createSourceEventStream, subscribe } from '../execute.js';
25+
import type { ExecutionResult } from '../IncrementalPublisher.js';
2526

2627
import { SimplePubSub } from './simplePubSub.js';
2728

0 commit comments

Comments
 (0)