Skip to content

Commit fc890c3

Browse files
committed
Raise a field error when encountering defer/stream directive while executing subscription
1 parent 880a21e commit fc890c3

File tree

5 files changed

+139
-29
lines changed

5 files changed

+139
-29
lines changed

src/execution/__tests__/defer-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ describe('Execute: defer directive', () => {
712712
rootValue: {},
713713
}),
714714
).to.throw(
715-
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive). Disable `@defer` or `@stream` by setting the `if` argument to `false`.',
715+
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)',
716716
);
717717
});
718718

@@ -735,7 +735,7 @@ describe('Execute: defer directive', () => {
735735
errors: [
736736
{
737737
message:
738-
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive). Disable `@defer` or `@stream` by setting the `if` argument to `false`.',
738+
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)',
739739
},
740740
],
741741
});

src/execution/__tests__/subscribe-test.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,12 @@ function createSubscription(
9797
variableValues?: { readonly [variable: string]: unknown },
9898
) {
9999
const document = parse(`
100-
subscription ($priority: Int = 0, $shouldDefer: Boolean = false, $asyncResolver: Boolean = false) {
100+
subscription (
101+
$priority: Int = 0
102+
$shouldDefer: Boolean = false
103+
$shouldStream: Boolean = false
104+
$asyncResolver: Boolean = false
105+
) {
101106
importantEmail(priority: $priority) {
102107
email {
103108
from
@@ -108,6 +113,7 @@ function createSubscription(
108113
}
109114
... @defer(if: $shouldDefer) {
110115
inbox {
116+
emails @include(if: $shouldStream) @stream(if: $shouldStream)
111117
unread
112118
total
113119
}
@@ -723,9 +729,12 @@ describe('Subscription Publish Phase', () => {
723729
errors: [
724730
{
725731
message:
726-
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive). Disable `@defer` or `@stream` by setting the `if` argument to `false`.',
732+
'`@defer` directive not supported on subscription operations. Disable `@defer` by setting the `if` argument to `false`.',
733+
locations: [{ line: 8, column: 7 }],
734+
path: ['importantEmail'],
727735
},
728736
],
737+
data: { importantEmail: null },
729738
},
730739
};
731740

@@ -757,6 +766,89 @@ describe('Subscription Publish Phase', () => {
757766
});
758767
});
759768

769+
it('subscribe function returns errors with @stream', async () => {
770+
const pubsub = new SimplePubSub<Email>();
771+
const subscription = await createSubscription(pubsub, {
772+
shouldStream: true,
773+
});
774+
assert(isAsyncIterable(subscription));
775+
// Wait for the next subscription payload.
776+
const payload = subscription.next();
777+
778+
// A new email arrives!
779+
expect(
780+
pubsub.emit({
781+
782+
subject: 'Alright',
783+
message: 'Tests are good',
784+
unread: true,
785+
}),
786+
).to.equal(true);
787+
788+
// The previously waited on payload now has a value.
789+
expectJSON(await payload).toDeepEqual({
790+
done: false,
791+
value: {
792+
errors: [
793+
{
794+
message:
795+
'`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.',
796+
locations: [{ line: 18, column: 13 }],
797+
path: ['importantEmail', 'inbox', 'emails'],
798+
},
799+
],
800+
data: {
801+
importantEmail: {
802+
email: { from: '[email protected]', subject: 'Alright' },
803+
inbox: { emails: null, unread: 1, total: 2 },
804+
},
805+
},
806+
},
807+
});
808+
809+
// Another new email arrives, after all incrementally delivered payloads are received.
810+
expect(
811+
pubsub.emit({
812+
813+
subject: 'Tools',
814+
message: 'I <3 making things',
815+
unread: true,
816+
}),
817+
).to.equal(true);
818+
819+
// The next waited on payload will have a value.
820+
expectJSON(await subscription.next()).toDeepEqual({
821+
done: false,
822+
value: {
823+
errors: [
824+
{
825+
message:
826+
'`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.',
827+
locations: [{ line: 18, column: 13 }],
828+
path: ['importantEmail', 'inbox', 'emails'],
829+
},
830+
],
831+
data: {
832+
importantEmail: {
833+
email: { from: '[email protected]', subject: 'Tools' },
834+
inbox: { emails: null, unread: 2, total: 3 },
835+
},
836+
},
837+
},
838+
});
839+
840+
expectJSON(await subscription.return()).toDeepEqual({
841+
done: true,
842+
value: undefined,
843+
});
844+
845+
// Awaiting a subscription after closing it results in completed results.
846+
expectJSON(await subscription.next()).toDeepEqual({
847+
done: true,
848+
value: undefined,
849+
});
850+
});
851+
760852
it('produces a payload when there are multiple events', async () => {
761853
const pubsub = new SimplePubSub<Email>();
762854
const subscription = createSubscription(pubsub);

src/execution/collectFields.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { AccumulatorMap } from '../jsutils/AccumulatorMap.js';
2+
import { invariant } from '../jsutils/invariant.js';
23
import type { ObjMap } from '../jsutils/ObjMap.js';
34

45
import type {
56
FieldNode,
67
FragmentDefinitionNode,
78
FragmentSpreadNode,
89
InlineFragmentNode,
10+
OperationDefinitionNode,
911
SelectionSetNode,
1012
} from '../language/ast.js';
13+
import { OperationTypeNode } from '../language/ast.js';
1114
import { Kind } from '../language/kinds.js';
1215

1316
import type { GraphQLObjectType } from '../type/definition.js';
@@ -47,16 +50,17 @@ export function collectFields(
4750
fragments: ObjMap<FragmentDefinitionNode>,
4851
variableValues: { [variable: string]: unknown },
4952
runtimeType: GraphQLObjectType,
50-
selectionSet: SelectionSetNode,
53+
operation: OperationDefinitionNode,
5154
): FieldsAndPatches {
5255
const fields = new AccumulatorMap<string, FieldNode>();
5356
const patches: Array<PatchFields> = [];
5457
collectFieldsImpl(
5558
schema,
5659
fragments,
5760
variableValues,
61+
operation,
5862
runtimeType,
59-
selectionSet,
63+
operation.selectionSet,
6064
fields,
6165
patches,
6266
new Set(),
@@ -74,10 +78,12 @@ export function collectFields(
7478
*
7579
* @internal
7680
*/
81+
// eslint-disable-next-line max-params
7782
export function collectSubfields(
7883
schema: GraphQLSchema,
7984
fragments: ObjMap<FragmentDefinitionNode>,
8085
variableValues: { [variable: string]: unknown },
86+
operation: OperationDefinitionNode,
8187
returnType: GraphQLObjectType,
8288
fieldNodes: ReadonlyArray<FieldNode>,
8389
): FieldsAndPatches {
@@ -96,6 +102,7 @@ export function collectSubfields(
96102
schema,
97103
fragments,
98104
variableValues,
105+
operation,
99106
returnType,
100107
node.selectionSet,
101108
subFieldNodes,
@@ -112,6 +119,7 @@ function collectFieldsImpl(
112119
schema: GraphQLSchema,
113120
fragments: ObjMap<FragmentDefinitionNode>,
114121
variableValues: { [variable: string]: unknown },
122+
operation: OperationDefinitionNode,
115123
runtimeType: GraphQLObjectType,
116124
selectionSet: SelectionSetNode,
117125
fields: AccumulatorMap<string, FieldNode>,
@@ -135,14 +143,15 @@ function collectFieldsImpl(
135143
continue;
136144
}
137145

138-
const defer = getDeferValues(variableValues, selection);
146+
const defer = getDeferValues(operation, variableValues, selection);
139147

140148
if (defer) {
141149
const patchFields = new AccumulatorMap<string, FieldNode>();
142150
collectFieldsImpl(
143151
schema,
144152
fragments,
145153
variableValues,
154+
operation,
146155
runtimeType,
147156
selection.selectionSet,
148157
patchFields,
@@ -158,6 +167,7 @@ function collectFieldsImpl(
158167
schema,
159168
fragments,
160169
variableValues,
170+
operation,
161171
runtimeType,
162172
selection.selectionSet,
163173
fields,
@@ -174,7 +184,7 @@ function collectFieldsImpl(
174184
continue;
175185
}
176186

177-
const defer = getDeferValues(variableValues, selection);
187+
const defer = getDeferValues(operation, variableValues, selection);
178188
if (visitedFragmentNames.has(fragName) && !defer) {
179189
continue;
180190
}
@@ -197,6 +207,7 @@ function collectFieldsImpl(
197207
schema,
198208
fragments,
199209
variableValues,
210+
operation,
200211
runtimeType,
201212
fragment.selectionSet,
202213
patchFields,
@@ -212,6 +223,7 @@ function collectFieldsImpl(
212223
schema,
213224
fragments,
214225
variableValues,
226+
operation,
215227
runtimeType,
216228
fragment.selectionSet,
217229
fields,
@@ -231,6 +243,7 @@ function collectFieldsImpl(
231243
* not disabled by the "if" argument.
232244
*/
233245
function getDeferValues(
246+
operation: OperationDefinitionNode,
234247
variableValues: { [variable: string]: unknown },
235248
node: FragmentSpreadNode | InlineFragmentNode,
236249
): undefined | { label: string | undefined } {
@@ -244,6 +257,11 @@ function getDeferValues(
244257
return;
245258
}
246259

260+
invariant(
261+
operation.operation !== OperationTypeNode.SUBSCRIPTION,
262+
'`@defer` directive not supported on subscription operations. Disable `@defer` by setting the `if` argument to `false`.',
263+
);
264+
247265
return {
248266
label: typeof defer.label === 'string' ? defer.label : undefined,
249267
};

src/execution/execute.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const collectSubfields = memoize3(
7878
exeContext.schema,
7979
exeContext.fragments,
8080
exeContext.variableValues,
81+
exeContext.operation,
8182
returnType,
8283
fieldNodes,
8384
),
@@ -263,7 +264,7 @@ export interface ExecutionArgs {
263264
}
264265

265266
const UNEXPECTED_MULTIPLE_PAYLOADS =
266-
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive). Disable `@defer` or `@stream` by setting the `if` argument to `false`.';
267+
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)';
267268

268269
/**
269270
* Implements the "Executing requests" section of the GraphQL specification.
@@ -531,7 +532,7 @@ function executeOperation(
531532
fragments,
532533
variableValues,
533534
rootType,
534-
operation.selectionSet,
535+
operation,
535536
);
536537
const path = undefined;
537538
let result;
@@ -992,6 +993,11 @@ function getStreamValues(
992993
'initialCount must be a positive integer',
993994
);
994995

996+
invariant(
997+
exeContext.operation.operation !== OperationTypeNode.SUBSCRIPTION,
998+
'`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.',
999+
);
1000+
9951001
return {
9961002
initialCount: stream.initialCount,
9971003
label: typeof stream.label === 'string' ? stream.label : undefined,
@@ -1529,17 +1535,6 @@ export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
15291535
}
15301536
};
15311537

1532-
function ensureSingleExecutionResult(
1533-
result: ExecutionResult | ExperimentalIncrementalExecutionResults,
1534-
): ExecutionResult {
1535-
if ('initialResult' in result) {
1536-
return {
1537-
errors: [new GraphQLError(UNEXPECTED_MULTIPLE_PAYLOADS)],
1538-
};
1539-
}
1540-
return result;
1541-
}
1542-
15431538
/**
15441539
* Implements the "Subscribe" algorithm described in the GraphQL specification.
15451540
*
@@ -1561,8 +1556,8 @@ function ensureSingleExecutionResult(
15611556
*
15621557
* This function does not support incremental delivery (`@defer` and `@stream`).
15631558
* If an operation which would defer or stream data is executed with this
1564-
* function, the result stream will be replaced with an `ExecutionResult`
1565-
* with a single error stating that defer/stream is not supported.
1559+
* function, a field error will be raised at the location of the `@defer` or
1560+
* `@stream` directive.
15661561
*
15671562
* Accepts an object with named arguments.
15681563
*/
@@ -1605,10 +1600,15 @@ function mapSourceToResponse(
16051600
// the GraphQL specification. The `execute` function provides the
16061601
// "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the
16071602
// "ExecuteQuery" algorithm, for which `execute` is also used.
1608-
return mapAsyncIterable(resultOrStream, async (payload: unknown) =>
1609-
ensureSingleExecutionResult(
1610-
await executeImpl(buildPerEventExecutionContext(exeContext, payload)),
1611-
),
1603+
return mapAsyncIterable(
1604+
resultOrStream,
1605+
(payload: unknown) =>
1606+
executeImpl(
1607+
buildPerEventExecutionContext(exeContext, payload),
1608+
// typecast to ExecutionResult, not possible to return
1609+
// ExperimentalIncrementalExecutionResults when
1610+
// exeContext.operation is 'subscription'.
1611+
) as ExecutionResult,
16121612
);
16131613
}
16141614

@@ -1689,7 +1689,7 @@ function executeSubscription(
16891689
fragments,
16901690
variableValues,
16911691
rootType,
1692-
operation.selectionSet,
1692+
operation,
16931693
);
16941694

16951695
const firstRootField = rootFields.entries().next().value;

src/validation/rules/SingleFieldSubscriptionsRule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function SingleFieldSubscriptionsRule(
4646
fragments,
4747
variableValues,
4848
subscriptionType,
49-
node.selectionSet,
49+
node,
5050
);
5151
if (fields.size > 1) {
5252
const fieldSelectionLists = [...fields.values()];

0 commit comments

Comments
 (0)