Skip to content

Commit 1c4477c

Browse files
authored
Spec compliance: Validation error on multi-field subscription (#882)
As per the spec, a subscription should simply use the first field defined, and not make any other assertions during the Subscribe operation. Instead, a validation rule should detect this and report it.
1 parent 0fe2972 commit 1c4477c

File tree

7 files changed

+156
-14
lines changed

7 files changed

+156
-14
lines changed

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ export {
271271
PossibleFragmentSpreadsRule,
272272
ProvidedNonNullArgumentsRule,
273273
ScalarLeafsRule,
274+
SingleFieldSubscriptionsRule,
274275
UniqueArgumentNamesRule,
275276
UniqueDirectivesPerLocationRule,
276277
UniqueFragmentNamesRule,

src/subscription/__tests__/subscribe-test.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,27 @@ describe('Subscribe', () => {
211211
}).not.to.throw();
212212
});
213213

214-
it('should throw when querying for multiple fields', async () => {
214+
it('should only resolve the first field of invalid multi-field', async () => {
215+
let didResolveImportantEmail = false;
216+
let didResolveNonImportantEmail = false;
217+
215218
const SubscriptionTypeMultiple = new GraphQLObjectType({
216219
name: 'Subscription',
217220
fields: {
218-
importantEmail: { type: EmailEventType },
219-
nonImportantEmail: { type: EmailEventType },
221+
importantEmail: {
222+
type: EmailEventType,
223+
subscribe() {
224+
didResolveImportantEmail = true;
225+
return eventEmitterAsyncIterator(new EventEmitter(), 'event');
226+
}
227+
},
228+
nonImportantEmail: {
229+
type: EmailEventType,
230+
subscribe() {
231+
didResolveNonImportantEmail = true;
232+
return eventEmitterAsyncIterator(new EventEmitter(), 'event');
233+
}
234+
},
220235
}
221236
});
222237

@@ -232,13 +247,14 @@ describe('Subscribe', () => {
232247
}
233248
`);
234249

235-
expect(() => {
236-
subscribe(
237-
testSchema,
238-
ast
239-
);
240-
}).to.throw(
241-
'A subscription operation must contain exactly one root field.');
250+
const subscription = subscribe(testSchema, ast);
251+
subscription.next(); // Ask for a result, but ignore it.
252+
253+
expect(didResolveImportantEmail).to.equal(true);
254+
expect(didResolveNonImportantEmail).to.equal(false);
255+
256+
// Close subscription
257+
subscription.return();
242258
});
243259

244260
it('produces payload for multiple subscribe in same subscription',

src/subscription/subscribe.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,6 @@ export function createSourceEventStream(
158158
Object.create(null)
159159
);
160160
const responseNames = Object.keys(fields);
161-
invariant(
162-
responseNames.length === 1,
163-
'A subscription operation must contain exactly one root field.'
164-
);
165161
const responseName = responseNames[0];
166162
const fieldNodes = fields[responseName];
167163
const fieldNode = fieldNodes[0];
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Copyright (c) 2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
import { describe, it } from 'mocha';
11+
import { expectPassesRule, expectFailsRule } from './harness';
12+
import {
13+
SingleFieldSubscriptions,
14+
singleFieldOnlyMessage,
15+
} from '../rules/SingleFieldSubscriptions';
16+
17+
18+
describe('Validate: Subscriptions with single field', () => {
19+
20+
it('valid subscription', () => {
21+
expectPassesRule(SingleFieldSubscriptions, `
22+
subscription ImportantEmails {
23+
importantEmails
24+
}
25+
`);
26+
});
27+
28+
it('fails with more than one root field', () => {
29+
expectFailsRule(SingleFieldSubscriptions, `
30+
subscription ImportantEmails {
31+
importantEmails
32+
notImportantEmails
33+
}
34+
`, [ {
35+
message: singleFieldOnlyMessage('ImportantEmails'),
36+
locations: [ { line: 4, column: 9 } ],
37+
path: undefined,
38+
} ]);
39+
});
40+
41+
it('fails with more than one root field including introspection', () => {
42+
expectFailsRule(SingleFieldSubscriptions, `
43+
subscription ImportantEmails {
44+
importantEmails
45+
__typename
46+
}
47+
`, [ {
48+
message: singleFieldOnlyMessage('ImportantEmails'),
49+
locations: [ { line: 4, column: 9 } ],
50+
path: undefined,
51+
} ]);
52+
});
53+
54+
it('fails with many more than one root field', () => {
55+
expectFailsRule(SingleFieldSubscriptions, `
56+
subscription ImportantEmails {
57+
importantEmails
58+
notImportantEmails
59+
spamEmails
60+
}
61+
`, [ {
62+
message: singleFieldOnlyMessage('ImportantEmails'),
63+
locations: [ { line: 4, column: 9 }, { line: 5, column: 9 } ],
64+
path: undefined,
65+
} ]);
66+
});
67+
68+
it('fails with more than one root field in anonymous subscriptions', () => {
69+
expectFailsRule(SingleFieldSubscriptions, `
70+
subscription {
71+
importantEmails
72+
notImportantEmails
73+
}
74+
`, [ {
75+
message: singleFieldOnlyMessage(null),
76+
locations: [ { line: 4, column: 9 } ],
77+
path: undefined,
78+
} ]);
79+
});
80+
81+
});

src/validation/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ export {
9696
ScalarLeafs as ScalarLeafsRule
9797
} from './rules/ScalarLeafs';
9898

99+
// Spec Section: "Subscriptions with Single Root Field"
100+
export {
101+
SingleFieldSubscriptions as SingleFieldSubscriptionsRule
102+
} from './rules/SingleFieldSubscriptions';
103+
99104
// Spec Section: "Argument Uniqueness"
100105
export {
101106
UniqueArgumentNames as UniqueArgumentNamesRule
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* @flow */
2+
/**
3+
* Copyright (c) 2015, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*/
10+
11+
import type { ValidationContext } from '../index';
12+
import { GraphQLError } from '../../error';
13+
import type { OperationDefinitionNode } from '../../language/ast';
14+
15+
16+
export function singleFieldOnlyMessage(name: ?string): string {
17+
return (name ? `Subscription "${name}" ` : 'Anonymous Subscription ') +
18+
'must select only one top level field.';
19+
}
20+
21+
/**
22+
* Subscriptions must only include one field.
23+
*
24+
* A GraphQL subscription is valid only if it contains a single root field.
25+
*/
26+
export function SingleFieldSubscriptions(context: ValidationContext): any {
27+
return {
28+
OperationDefinition(node: OperationDefinitionNode) {
29+
if (node.operation === 'subscription') {
30+
if (node.selectionSet.selections.length !== 1) {
31+
context.reportError(new GraphQLError(
32+
singleFieldOnlyMessage(node.name && node.name.value),
33+
node.selectionSet.selections.slice(1)
34+
));
35+
}
36+
}
37+
}
38+
};
39+
}

src/validation/specifiedRules.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import { UniqueOperationNames } from './rules/UniqueOperationNames';
1414
// Spec Section: "Lone Anonymous Operation"
1515
import { LoneAnonymousOperation } from './rules/LoneAnonymousOperation';
1616

17+
// Spec Section: "Subscriptions with Single Root Field"
18+
import { SingleFieldSubscriptions } from './rules/SingleFieldSubscriptions';
19+
1720
// Spec Section: "Fragment Spread Type Existence"
1821
import { KnownTypeNames } from './rules/KnownTypeNames';
1922

@@ -98,6 +101,7 @@ import type { ValidationContext } from './index';
98101
export const specifiedRules: Array<(context: ValidationContext) => any> = [
99102
UniqueOperationNames,
100103
LoneAnonymousOperation,
104+
SingleFieldSubscriptions,
101105
KnownTypeNames,
102106
FragmentsOnCompositeTypes,
103107
VariablesAreInputTypes,

0 commit comments

Comments
 (0)