Skip to content

Commit e528a7e

Browse files
committed
Raise a field error when encountering defer/stream directive while executing subscription
1 parent 7736a1a commit e528a7e

File tree

5 files changed

+137
-27
lines changed

5 files changed

+137
-27
lines changed

src/execution/__tests__/defer-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ describe('Execute: defer directive', () => {
670670
rootValue: {},
671671
}),
672672
).to.throw(
673-
'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`.',
673+
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)',
674674
);
675675
});
676676

@@ -693,7 +693,7 @@ describe('Execute: defer directive', () => {
693693
errors: [
694694
{
695695
message:
696-
'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`.',
696+
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)',
697697
},
698698
],
699699
});

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: 3, 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: 13, 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: 13, 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: 18 additions & 18 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.
@@ -530,7 +531,7 @@ function executeOperation(
530531
fragments,
531532
variableValues,
532533
rootType,
533-
operation.selectionSet,
534+
operation,
534535
);
535536
const path = undefined;
536537
let result;
@@ -949,6 +950,11 @@ function getStreamValues(
949950
'initialCount must be a positive integer',
950951
);
951952

953+
invariant(
954+
exeContext.operation.operation !== OperationTypeNode.SUBSCRIPTION,
955+
'`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.',
956+
);
957+
952958
return {
953959
initialCount: stream.initialCount,
954960
label: typeof stream.label === 'string' ? stream.label : undefined,
@@ -1456,17 +1462,6 @@ export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
14561462
}
14571463
};
14581464

1459-
function ensureSingleExecutionResult(
1460-
result: ExecutionResult | ExperimentalIncrementalExecutionResults,
1461-
): ExecutionResult {
1462-
if ('initialResult' in result) {
1463-
return {
1464-
errors: [new GraphQLError(UNEXPECTED_MULTIPLE_PAYLOADS)],
1465-
};
1466-
}
1467-
return result;
1468-
}
1469-
14701465
/**
14711466
* Implements the "Subscribe" algorithm described in the GraphQL specification.
14721467
*
@@ -1534,10 +1529,15 @@ function mapSourceToResponse(
15341529
// the GraphQL specification. The `execute` function provides the
15351530
// "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the
15361531
// "ExecuteQuery" algorithm, for which `execute` is also used.
1537-
return mapAsyncIterable(resultOrStream, async (payload: unknown) =>
1538-
ensureSingleExecutionResult(
1539-
await executeImpl(buildPerEventExecutionContext(exeContext, payload)),
1540-
),
1532+
return mapAsyncIterable(
1533+
resultOrStream,
1534+
(payload: unknown) =>
1535+
executeImpl(
1536+
buildPerEventExecutionContext(exeContext, payload),
1537+
// typecast to ExecutionResult, not possible to return
1538+
// ExperimentalIncrementalExecutionResults when
1539+
// exeContext.operation is 'subscription'.
1540+
) as ExecutionResult,
15411541
);
15421542
}
15431543

@@ -1618,7 +1618,7 @@ function executeSubscription(
16181618
fragments,
16191619
variableValues,
16201620
rootType,
1621-
operation.selectionSet,
1621+
operation,
16221622
);
16231623

16241624
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)