Skip to content

Commit 3e4954e

Browse files
committed
Make allowEmptySelectionSet the default
1 parent d91a00e commit 3e4954e

9 files changed

Lines changed: 41 additions & 137 deletions

File tree

src/execution/__tests__/executor-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,14 +234,14 @@ describe('Execute: Handles basic execution tasks', () => {
234234
const schema = new GraphQLSchema({ query: Type });
235235

236236
const document = parse('{ a deep { } ...Frag } fragment Frag on Type { }', {
237-
experimentalEmptySelectionSets: true,
237+
allowEmptySelectionSets: true,
238238
});
239239

240240
const result = executeSync({ schema, document });
241241
expect(result).to.deep.equal({ data: { a: 'Apple', deep: {} } });
242242

243243
const emptyDocument = parse('{ }', {
244-
experimentalEmptySelectionSets: true,
244+
allowEmptySelectionSets: true,
245245
});
246246
expect(executeSync({ schema, document: emptyDocument })).to.deep.equal({
247247
data: {},

src/language/__tests__/parser-test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
OperationDefinitionNode,
1818
} from '../ast.ts';
1919
import { Kind } from '../kinds.ts';
20+
import type { ParseOptions } from '../parser.ts';
2021
import {
2122
parse,
2223
parseConstValue,
@@ -27,8 +28,8 @@ import {
2728
import { Source } from '../source.ts';
2829
import { TokenKind } from '../tokenKind.ts';
2930

30-
function expectSyntaxError(text: string) {
31-
return expectToThrowJSON(() => parse(text));
31+
function expectSyntaxError(text: string, options?: ParseOptions) {
32+
return expectToThrowJSON(() => parse(text, options));
3233
}
3334

3435
describe('Parser', () => {
@@ -508,7 +509,6 @@ describe('Parser', () => {
508509

509510
it('allows parsing empty selection sets', () => {
510511
const document = parse('{ node { } }', {
511-
experimentalEmptySelectionSets: true,
512512
noLocation: true,
513513
});
514514

@@ -545,7 +545,6 @@ describe('Parser', () => {
545545

546546
it('allows parsing an empty operation selection set', () => {
547547
const document = parse('{ }', {
548-
experimentalEmptySelectionSets: true,
549548
noLocation: true,
550549
});
551550
const operation = document.definitions[0] as OperationDefinitionNode;
@@ -555,16 +554,17 @@ describe('Parser', () => {
555554

556555
it('allows parsing an empty fragment selection set', () => {
557556
const document = parse('fragment a on t { }', {
558-
experimentalEmptySelectionSets: true,
559557
noLocation: true,
560558
});
561559
const fragment = document.definitions[0] as FragmentDefinitionNode;
562560

563561
expect(fragment.selectionSet.selections).to.deep.equal([]);
564562
});
565563

566-
it('disallows parsing empty selection sets without experimental flag', () => {
567-
expectSyntaxError('{ node { } }').to.deep.equal({
564+
it('disallows parsing empty selection sets when disabled', () => {
565+
expectSyntaxError('{ node { } }', {
566+
allowEmptySelectionSets: false,
567+
}).to.deep.equal({
568568
message: 'Syntax Error: Expected Name, found "}".',
569569
locations: [{ line: 1, column: 10 }],
570570
});

src/language/__tests__/printer-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('Printer: Query document', () => {
2020

2121
it('prints empty selection sets', () => {
2222
const ast = parse('{ foo { } bar { ... on Baz { } } }', {
23-
experimentalEmptySelectionSets: true,
23+
allowEmptySelectionSets: true,
2424
});
2525
expect(print(ast)).to.equal(dedent`
2626
{
@@ -32,7 +32,7 @@ describe('Printer: Query document', () => {
3232
`);
3333

3434
const emptyOperationAST = parse('{ }', {
35-
experimentalEmptySelectionSets: true,
35+
allowEmptySelectionSets: true,
3636
});
3737
expect(print(emptyOperationAST)).to.equal('{}');
3838
});

src/language/parser.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,9 @@ export interface ParseOptions {
119119
experimentalFragmentArguments?: boolean | undefined;
120120

121121
/**
122-
* EXPERIMENTAL:
123-
*
124-
* If enabled, the parser accepts selection sets that contain no
125-
* selections, changing the grammar from `SelectionSet : { Selection+ }` to
126-
* `SelectionSet : { Selection* }`.
122+
* If enabled (the default), the parser accepts selection sets that contain
123+
* no selections, changing the grammar from `SelectionSet : { Selection+ }`
124+
* to `SelectionSet : { Selection* }`.
127125
*
128126
* See https://github.com/graphql/graphql-spec/pull/1227
129127
* @example
@@ -133,7 +131,7 @@ export interface ParseOptions {
133131
* }
134132
* ```
135133
*/
136-
experimentalEmptySelectionSets?: boolean | undefined;
134+
allowEmptySelectionSets?: boolean | undefined;
137135

138136
/**
139137
* Internal parser hook for GraphQL.js entry points that need to parse a
@@ -595,7 +593,7 @@ export class Parser {
595593
* SelectionSet : { Selection+ }
596594
* ```
597595
*
598-
* With `experimentalEmptySelectionSets` enabled:
596+
* With `allowEmptySelectionSets` enabled (the default):
599597
*
600598
* ```
601599
* SelectionSet : { Selection* }
@@ -607,13 +605,9 @@ export class Parser {
607605
return this.node<SelectionSetNode>(this._lexer.token, {
608606
kind: Kind.SELECTION_SET,
609607
selections:
610-
this._options.experimentalEmptySelectionSets === true
611-
? this.any(TokenKind.BRACE_L, this.parseSelection, TokenKind.BRACE_R)
612-
: this.many(
613-
TokenKind.BRACE_L,
614-
this.parseSelection,
615-
TokenKind.BRACE_R,
616-
),
608+
this._options.allowEmptySelectionSets === false
609+
? this.many(TokenKind.BRACE_L, this.parseSelection, TokenKind.BRACE_R)
610+
: this.any(TokenKind.BRACE_L, this.parseSelection, TokenKind.BRACE_R),
617611
});
618612
}
619613

src/validation/ValidationContext.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ interface ValidationContextOptions {
181181
/** Whether suggestion text should be omitted from errors. */
182182
hideSuggestions?: Maybe<boolean>;
183183
/** Whether selection sets on composite types may be empty. */
184-
experimentalEmptySelectionSets?: Maybe<boolean>;
184+
allowEmptySelectionSets?: Maybe<boolean>;
185185
}
186186

187187
/** Validation context passed to query validation rules. */
@@ -198,7 +198,7 @@ export class ValidationContext extends ASTValidationContext {
198198
ReadonlyArray<VariableUsage>
199199
>;
200200
private _hideSuggestions: boolean;
201-
private _experimentalEmptySelectionSets: boolean;
201+
private _allowEmptySelectionSets: boolean;
202202

203203
/**
204204
* Creates a ValidationContext instance.
@@ -262,15 +262,14 @@ export class ValidationContext extends ASTValidationContext {
262262
typeof hideSuggestionsOrOptions === 'boolean'
263263
? { hideSuggestions: hideSuggestionsOrOptions }
264264
: (hideSuggestionsOrOptions ?? {});
265-
const { hideSuggestions, experimentalEmptySelectionSets } = options;
265+
const { hideSuggestions, allowEmptySelectionSets } = options;
266266
super(ast, onError);
267267
this._schema = schema;
268268
this._typeInfo = typeInfo;
269269
this._variableUsages = new Map();
270270
this._recursiveVariableUsages = new Map();
271271
this._hideSuggestions = hideSuggestions ?? false;
272-
this._experimentalEmptySelectionSets =
273-
experimentalEmptySelectionSets ?? false;
272+
this._allowEmptySelectionSets = allowEmptySelectionSets ?? true;
274273
}
275274

276275
/**
@@ -291,13 +290,10 @@ export class ValidationContext extends ASTValidationContext {
291290

292291
/**
293292
* Returns whether empty selection sets on composite types are allowed.
294-
*
295-
* Note: empty selection sets are experimental and may be changed or removed
296-
* in the future.
297293
* @returns True when a selection set on a composite type may be empty.
298294
*/
299-
get experimentalEmptySelectionSets(): boolean {
300-
return this._experimentalEmptySelectionSets;
295+
get allowEmptySelectionSets(): boolean {
296+
return this._allowEmptySelectionSets;
301297
}
302298

303299
/**

src/validation/__tests__/ScalarLeafsRule-test.ts

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ describe('Validate: Scalar leafs', () => {
6767
// We can't leverage expectErrors since it doesn't support passing in the
6868
// documentNode directly. We have to do this because this is technically
6969
// an invalid document.
70-
const errors = validate(testSchema, doc, [ScalarLeafsRule]);
70+
const errors = validate(testSchema, doc, [ScalarLeafsRule], {
71+
allowEmptySelectionSets: false,
72+
});
7173
expectJSON(errors).toDeepEqual([
7274
{
7375
message:
@@ -76,25 +78,17 @@ describe('Validate: Scalar leafs', () => {
7678
]);
7779
});
7880

79-
it('object type having no selections is allowed with experimentalEmptySelectionSets', () => {
80-
const doc = parse('{ human { } }', {
81-
experimentalEmptySelectionSets: true,
82-
});
81+
it('object type having no selections is allowed by default', () => {
82+
const doc = parse('{ human { } }');
8383

84-
const errors = validate(testSchema, doc, [ScalarLeafsRule], {
85-
experimentalEmptySelectionSets: true,
86-
});
84+
const errors = validate(testSchema, doc, [ScalarLeafsRule]);
8785
expectJSON(errors).toDeepEqual([]);
8886
});
8987

90-
it('scalar selection is still rejected with experimentalEmptySelectionSets', () => {
91-
const doc = parse('{ human { name { } } }', {
92-
experimentalEmptySelectionSets: true,
93-
});
88+
it('scalar selection is still rejected with allowEmptySelectionSets', () => {
89+
const doc = parse('{ human { name { } } }');
9490

95-
const errors = validate(testSchema, doc, [ScalarLeafsRule], {
96-
experimentalEmptySelectionSets: true,
97-
});
91+
const errors = validate(testSchema, doc, [ScalarLeafsRule]);
9892
expectJSON(errors).toDeepEqual([
9993
{
10094
message:
@@ -195,37 +189,4 @@ describe('Validate: Scalar leafs', () => {
195189
},
196190
]);
197191
});
198-
199-
it('object type having only one selection', () => {
200-
const doc: DocumentNode = {
201-
kind: Kind.DOCUMENT,
202-
definitions: [
203-
{
204-
kind: Kind.OPERATION_DEFINITION,
205-
operation: OperationTypeNode.QUERY,
206-
selectionSet: {
207-
kind: Kind.SELECTION_SET,
208-
selections: [
209-
{
210-
kind: Kind.FIELD,
211-
name: { kind: Kind.NAME, value: 'human' },
212-
selectionSet: { kind: Kind.SELECTION_SET, selections: [] },
213-
},
214-
],
215-
},
216-
},
217-
],
218-
};
219-
220-
// We can't leverage expectErrors since it doesn't support passing in the
221-
// documentNode directly. We have to do this because this is technically
222-
// an invalid document.
223-
const errors = validate(testSchema, doc, [ScalarLeafsRule]);
224-
expectJSON(errors).toDeepEqual([
225-
{
226-
message:
227-
'Field "human" of type "Human" must have at least one field selected.',
228-
},
229-
]);
230-
});
231192
});

src/validation/rules/ScalarLeafsRule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function ScalarLeafsRule(context: ValidationContext): ASTVisitor {
7272
);
7373
} else if (
7474
selectionSet.selections.length === 0 &&
75-
!context.experimentalEmptySelectionSets
75+
!context.allowEmptySelectionSets
7676
) {
7777
const fieldName = node.name.value;
7878
const typeStr = inspect(type);

src/validation/validate.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,14 @@ export interface ValidationOptions {
3333
/** Whether suggestion text should be omitted from validation errors. */
3434
hideSuggestions?: Maybe<boolean>;
3535
/**
36-
* EXPERIMENTAL:
36+
* If enabled (the default), a selection set on an object, interface or
37+
* union type is only required to be present, rather than to be non-empty.
3738
*
38-
* If enabled, a selection set on an object, interface or union type is only
39-
* required to be present, rather than to be non-empty.
40-
*
41-
* Pair this with the `experimentalEmptySelectionSets` parse option.
39+
* Pair this with the `allowEmptySelectionSets` parse option.
4240
*
4341
* See https://github.com/graphql/graphql-spec/pull/1227
4442
*/
45-
experimentalEmptySelectionSets?: boolean | undefined;
43+
allowEmptySelectionSets?: boolean | undefined;
4644
}
4745

4846
// Per the specification, descriptions must not affect validation.
@@ -148,8 +146,7 @@ function validateImpl(
148146
): ReadonlyArray<GraphQLError> {
149147
const maxErrors = options?.maxErrors ?? 100;
150148
const hideSuggestions = options?.hideSuggestions ?? false;
151-
const experimentalEmptySelectionSets =
152-
options?.experimentalEmptySelectionSets ?? false;
149+
const allowEmptySelectionSets = options?.allowEmptySelectionSets ?? true;
153150

154151
// If the schema used for validation is invalid, throw an error.
155152
assertValidSchema(schema);
@@ -166,7 +163,7 @@ function validateImpl(
166163
}
167164
errors.push(error);
168165
},
169-
{ hideSuggestions, experimentalEmptySelectionSets },
166+
{ hideSuggestions, allowEmptySelectionSets },
170167
);
171168

172169
// This uses a specialized visitor which runs multiple visitors in parallel,

website/pages/docs/experimental-specification-features.mdx

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -65,47 +65,3 @@ fragment spreads pass values for them. GraphQL.js exposes the syntax through the
6565
`FragmentArgumentNode`, and execution supports the resulting values.
6666

6767
See [Fragment Arguments](/docs/fragment-arguments).
68-
69-
## Empty selection sets
70-
71-
Empty selection sets relax the grammar from `SelectionSet : { Selection+ }` to
72-
`SelectionSet : { Selection* }`, so a selection set on an object, interface, or
73-
union type only has to be present rather than non-empty. Leaf fields are
74-
unaffected: a scalar or enum field must still have no selection set at all, and
75-
`name { }` remains invalid.
76-
77-
This helps clients that strip fields during query preprocessing. When every
78-
server-directed field of a selection set is handled locally (Apollo's `@client`,
79-
Relay Resolvers, and similar client-side extensions), what remains is an empty
80-
selection set, and the client no longer has to inject a placeholder field such
81-
as `__typename` or rewrite the enclosing selection away.
82-
83-
GraphQL.js exposes the syntax through the `experimentalEmptySelectionSets`
84-
parser option and the matching `experimentalEmptySelectionSets` validation
85-
option; `graphql()` and `graphqlSync()` accept both from their single arguments
86-
object. Execution needs no flag — an empty selection set resolves to an empty
87-
object.
88-
89-
```js
90-
import { graphql, buildSchema } from 'graphql';
91-
92-
const schema = buildSchema(`
93-
type Query {
94-
viewer: Viewer
95-
}
96-
97-
type Viewer {
98-
id: ID
99-
}
100-
`);
101-
102-
const result = await graphql({
103-
schema,
104-
source: '{ viewer { } }',
105-
rootValue: { viewer: { id: '1' } },
106-
experimentalEmptySelectionSets: true,
107-
});
108-
// { data: { viewer: {} } }
109-
```
110-
111-
See [the specification proposal](https://github.com/graphql/graphql-spec/pull/1227).

0 commit comments

Comments
 (0)