Skip to content

Commit 5dc773e

Browse files
authored
RFC: SchemaExtension (#1314)
* RFC: SchemaExtension This adds support for graphql/graphql-spec#428 spec proposal. So far this just adds language support and updates validation rules to be aware of this new ast node. I'll follow up with support in `extendSchema()` and tests. * Support extendSchema() * Formatting edits * Add parsing and validation tests * Adjust grammar rules to match spec definitions
1 parent 3cb4500 commit 5dc773e

17 files changed

+416
-63
lines changed

src/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,14 +245,16 @@ export type {
245245
EnumTypeDefinitionNode,
246246
EnumValueDefinitionNode,
247247
InputObjectTypeDefinitionNode,
248+
DirectiveDefinitionNode,
249+
TypeSystemExtensionNode,
250+
SchemaExtensionNode,
248251
TypeExtensionNode,
249252
ScalarTypeExtensionNode,
250253
ObjectTypeExtensionNode,
251254
InterfaceTypeExtensionNode,
252255
UnionTypeExtensionNode,
253256
EnumTypeExtensionNode,
254257
InputObjectTypeExtensionNode,
255-
DirectiveDefinitionNode,
256258
KindEnum,
257259
TokenKindEnum,
258260
DirectiveLocationEnum,

src/language/__tests__/schema-kitchen-sink.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,9 @@ directive @include2(if: Boolean!) on
123123
| FIELD
124124
| FRAGMENT_SPREAD
125125
| INLINE_FRAGMENT
126+
127+
extend schema @onSchema
128+
129+
extend schema @onSchema {
130+
subscription: SubscriptionType
131+
}

src/language/__tests__/schema-parser-test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,66 @@ extend type Hello {
263263
);
264264
});
265265

266+
it('Schema extension', () => {
267+
const body = `
268+
extend schema {
269+
mutation: Mutation
270+
}`;
271+
const doc = parse(body);
272+
const expected = {
273+
kind: 'Document',
274+
definitions: [
275+
{
276+
kind: 'SchemaExtension',
277+
directives: [],
278+
operationTypes: [
279+
{
280+
kind: 'OperationTypeDefinition',
281+
operation: 'mutation',
282+
type: typeNode('Mutation', { start: 41, end: 49 }),
283+
loc: { start: 31, end: 49 },
284+
},
285+
],
286+
loc: { start: 7, end: 57 },
287+
},
288+
],
289+
loc: { start: 0, end: 57 },
290+
};
291+
expect(printJson(doc)).to.equal(printJson(expected));
292+
});
293+
294+
it('Schema extension with only directives', () => {
295+
const body = 'extend schema @directive';
296+
const doc = parse(body);
297+
const expected = {
298+
kind: 'Document',
299+
definitions: [
300+
{
301+
kind: 'SchemaExtension',
302+
directives: [
303+
{
304+
kind: 'Directive',
305+
name: nameNode('directive', { start: 15, end: 24 }),
306+
arguments: [],
307+
loc: { start: 14, end: 24 },
308+
},
309+
],
310+
operationTypes: [],
311+
loc: { start: 0, end: 24 },
312+
},
313+
],
314+
loc: { start: 0, end: 24 },
315+
};
316+
expect(printJson(doc)).to.equal(printJson(expected));
317+
});
318+
319+
it('Schema extension without anything throws', () => {
320+
expectSyntaxError('extend schema', 'Unexpected <EOF>', {
321+
line: 1,
322+
column: 14,
323+
});
324+
});
325+
266326
it('Simple non-null type', () => {
267327
const body = `
268328
type Hello {

src/language/__tests__/schema-printer-test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ describe('Printer: SDL document', () => {
161161
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
162162
163163
directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
164+
165+
extend schema @onSchema
166+
167+
extend schema @onSchema {
168+
subscription: SubscriptionType
169+
}
164170
`);
165171
});
166172
});

src/language/ast.js

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,14 @@ export type ASTNode =
124124
| EnumTypeDefinitionNode
125125
| EnumValueDefinitionNode
126126
| InputObjectTypeDefinitionNode
127+
| DirectiveDefinitionNode
128+
| SchemaExtensionNode
127129
| ScalarTypeExtensionNode
128130
| ObjectTypeExtensionNode
129131
| InterfaceTypeExtensionNode
130132
| UnionTypeExtensionNode
131133
| EnumTypeExtensionNode
132-
| InputObjectTypeExtensionNode
133-
| DirectiveDefinitionNode;
134+
| InputObjectTypeExtensionNode;
134135

135136
/**
136137
* Utility type listing all nodes indexed by their kind.
@@ -171,13 +172,14 @@ export type ASTKindToNode = {
171172
EnumTypeDefinition: EnumTypeDefinitionNode,
172173
EnumValueDefinition: EnumValueDefinitionNode,
173174
InputObjectTypeDefinition: InputObjectTypeDefinitionNode,
175+
DirectiveDefinition: DirectiveDefinitionNode,
176+
SchemaExtension: SchemaExtensionNode,
174177
ScalarTypeExtension: ScalarTypeExtensionNode,
175178
ObjectTypeExtension: ObjectTypeExtensionNode,
176179
InterfaceTypeExtension: InterfaceTypeExtensionNode,
177180
UnionTypeExtension: UnionTypeExtensionNode,
178181
EnumTypeExtension: EnumTypeExtensionNode,
179182
InputObjectTypeExtension: InputObjectTypeExtensionNode,
180-
DirectiveDefinition: DirectiveDefinitionNode,
181183
};
182184

183185
// Name
@@ -198,7 +200,8 @@ export type DocumentNode = {
198200

199201
export type DefinitionNode =
200202
| ExecutableDefinitionNode
201-
| TypeSystemDefinitionNode; // experimental non-spec addition.
203+
| TypeSystemDefinitionNode
204+
| TypeSystemExtensionNode;
202205

203206
export type ExecutableDefinitionNode =
204207
| OperationDefinitionNode
@@ -388,13 +391,12 @@ export type NonNullTypeNode = {
388391
export type TypeSystemDefinitionNode =
389392
| SchemaDefinitionNode
390393
| TypeDefinitionNode
391-
| TypeExtensionNode
392394
| DirectiveDefinitionNode;
393395

394396
export type SchemaDefinitionNode = {
395397
+kind: 'SchemaDefinition',
396398
+loc?: Location,
397-
+directives: $ReadOnlyArray<DirectiveNode>,
399+
+directives?: $ReadOnlyArray<DirectiveNode>,
398400
+operationTypes: $ReadOnlyArray<OperationTypeDefinitionNode>,
399401
};
400402

@@ -497,6 +499,28 @@ export type InputObjectTypeDefinitionNode = {
497499
+fields?: $ReadOnlyArray<InputValueDefinitionNode>,
498500
};
499501

502+
// Directive Definitions
503+
504+
export type DirectiveDefinitionNode = {
505+
+kind: 'DirectiveDefinition',
506+
+loc?: Location,
507+
+description?: StringValueNode,
508+
+name: NameNode,
509+
+arguments?: $ReadOnlyArray<InputValueDefinitionNode>,
510+
+locations: $ReadOnlyArray<NameNode>,
511+
};
512+
513+
// Type System Extensions
514+
515+
export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode;
516+
517+
export type SchemaExtensionNode = {
518+
+kind: 'SchemaExtension',
519+
+loc?: Location,
520+
+directives?: $ReadOnlyArray<DirectiveNode>,
521+
+operationTypes?: $ReadOnlyArray<OperationTypeDefinitionNode>,
522+
};
523+
500524
// Type Extensions
501525

502526
export type TypeExtensionNode =
@@ -554,14 +578,3 @@ export type InputObjectTypeExtensionNode = {
554578
+directives?: $ReadOnlyArray<DirectiveNode>,
555579
+fields?: $ReadOnlyArray<InputValueDefinitionNode>,
556580
};
557-
558-
// Directive Definitions
559-
560-
export type DirectiveDefinitionNode = {
561-
+kind: 'DirectiveDefinition',
562-
+loc?: Location,
563-
+description?: StringValueNode,
564-
+name: NameNode,
565-
+arguments?: $ReadOnlyArray<InputValueDefinitionNode>,
566-
+locations: $ReadOnlyArray<NameNode>,
567-
};

src/language/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,16 @@ export type {
7676
EnumTypeDefinitionNode,
7777
EnumValueDefinitionNode,
7878
InputObjectTypeDefinitionNode,
79+
DirectiveDefinitionNode,
80+
TypeSystemExtensionNode,
81+
SchemaExtensionNode,
7982
TypeExtensionNode,
8083
ScalarTypeExtensionNode,
8184
ObjectTypeExtensionNode,
8285
InterfaceTypeExtensionNode,
8386
UnionTypeExtensionNode,
8487
EnumTypeExtensionNode,
8588
InputObjectTypeExtensionNode,
86-
DirectiveDefinitionNode,
8789
} from './ast';
8890

8991
export { DirectiveLocation } from './directiveLocation';

src/language/kinds.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,19 @@ export const Kind = Object.freeze({
6262
ENUM_VALUE_DEFINITION: 'EnumValueDefinition',
6363
INPUT_OBJECT_TYPE_DEFINITION: 'InputObjectTypeDefinition',
6464

65+
// Directive Definitions
66+
DIRECTIVE_DEFINITION: 'DirectiveDefinition',
67+
68+
// Type System Extensions
69+
SCHEMA_EXTENSION: 'SchemaExtension',
70+
6571
// Type Extensions
6672
SCALAR_TYPE_EXTENSION: 'ScalarTypeExtension',
6773
OBJECT_TYPE_EXTENSION: 'ObjectTypeExtension',
6874
INTERFACE_TYPE_EXTENSION: 'InterfaceTypeExtension',
6975
UNION_TYPE_EXTENSION: 'UnionTypeExtension',
7076
ENUM_TYPE_EXTENSION: 'EnumTypeExtension',
7177
INPUT_OBJECT_TYPE_EXTENSION: 'InputObjectTypeExtension',
72-
73-
// Directive Definitions
74-
DIRECTIVE_DEFINITION: 'DirectiveDefinition',
7578
});
7679

7780
/**

src/language/parser.js

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,15 @@ import type {
5252
EnumTypeDefinitionNode,
5353
EnumValueDefinitionNode,
5454
InputObjectTypeDefinitionNode,
55-
TypeExtensionNode,
55+
DirectiveDefinitionNode,
56+
TypeSystemExtensionNode,
57+
SchemaExtensionNode,
5658
ScalarTypeExtensionNode,
5759
ObjectTypeExtensionNode,
5860
InterfaceTypeExtensionNode,
5961
UnionTypeExtensionNode,
6062
EnumTypeExtensionNode,
6163
InputObjectTypeExtensionNode,
62-
DirectiveDefinitionNode,
6364
} from './ast';
6465

6566
import { Kind } from './kinds';
@@ -211,6 +212,7 @@ function parseDocument(lexer: Lexer<*>): DocumentNode {
211212
* Definition :
212213
* - ExecutableDefinition
213214
* - TypeSystemDefinition
215+
* - TypeSystemExtension
214216
*/
215217
function parseDefinition(lexer: Lexer<*>): DefinitionNode {
216218
if (peek(lexer, TokenKind.NAME)) {
@@ -227,15 +229,14 @@ function parseDefinition(lexer: Lexer<*>): DefinitionNode {
227229
case 'union':
228230
case 'enum':
229231
case 'input':
230-
case 'extend':
231232
case 'directive':
232-
// Note: The schema definition language is an experimental addition.
233233
return parseTypeSystemDefinition(lexer);
234+
case 'extend':
235+
return parseTypeSystemExtension(lexer);
234236
}
235237
} else if (peek(lexer, TokenKind.BRACE_L)) {
236238
return parseExecutableDefinition(lexer);
237239
} else if (peekDescription(lexer)) {
238-
// Note: The schema definition language is an experimental addition.
239240
return parseTypeSystemDefinition(lexer);
240241
}
241242

@@ -753,7 +754,6 @@ export function parseNamedType(lexer: Lexer<*>): NamedTypeNode {
753754
* TypeSystemDefinition :
754755
* - SchemaDefinition
755756
* - TypeDefinition
756-
* - TypeExtension
757757
* - DirectiveDefinition
758758
*
759759
* TypeDefinition :
@@ -784,8 +784,6 @@ function parseTypeSystemDefinition(lexer: Lexer<*>): TypeSystemDefinitionNode {
784784
return parseEnumTypeDefinition(lexer);
785785
case 'input':
786786
return parseInputObjectTypeDefinition(lexer);
787-
case 'extend':
788-
return parseTypeExtension(lexer);
789787
case 'directive':
790788
return parseDirectiveDefinition(lexer);
791789
}
@@ -1141,6 +1139,10 @@ function parseInputFieldsDefinition(
11411139
}
11421140

11431141
/**
1142+
* TypeSystemExtension :
1143+
* - SchemaExtension
1144+
* - TypeExtension
1145+
*
11441146
* TypeExtension :
11451147
* - ScalarTypeExtension
11461148
* - ObjectTypeExtension
@@ -1149,11 +1151,13 @@ function parseInputFieldsDefinition(
11491151
* - EnumTypeExtension
11501152
* - InputObjectTypeDefinition
11511153
*/
1152-
function parseTypeExtension(lexer: Lexer<*>): TypeExtensionNode {
1154+
function parseTypeSystemExtension(lexer: Lexer<*>): TypeSystemExtensionNode {
11531155
const keywordToken = lexer.lookahead();
11541156

11551157
if (keywordToken.kind === TokenKind.NAME) {
11561158
switch (keywordToken.value) {
1159+
case 'schema':
1160+
return parseSchemaExtension(lexer);
11571161
case 'scalar':
11581162
return parseScalarTypeExtension(lexer);
11591163
case 'type':
@@ -1172,6 +1176,35 @@ function parseTypeExtension(lexer: Lexer<*>): TypeExtensionNode {
11721176
throw unexpected(lexer, keywordToken);
11731177
}
11741178

1179+
/**
1180+
* SchemaExtension :
1181+
* - extend schema Directives[Const]? { OperationTypeDefinition+ }
1182+
* - extend schema Directives[Const]
1183+
*/
1184+
function parseSchemaExtension(lexer: Lexer<*>): SchemaExtensionNode {
1185+
const start = lexer.token;
1186+
expectKeyword(lexer, 'extend');
1187+
expectKeyword(lexer, 'schema');
1188+
const directives = parseDirectives(lexer, true);
1189+
const operationTypes = peek(lexer, TokenKind.BRACE_L)
1190+
? many(
1191+
lexer,
1192+
TokenKind.BRACE_L,
1193+
parseOperationTypeDefinition,
1194+
TokenKind.BRACE_R,
1195+
)
1196+
: [];
1197+
if (directives.length === 0 && operationTypes.length === 0) {
1198+
throw unexpected(lexer);
1199+
}
1200+
return {
1201+
kind: Kind.SCHEMA_EXTENSION,
1202+
directives,
1203+
operationTypes,
1204+
loc: loc(lexer, start),
1205+
};
1206+
}
1207+
11751208
/**
11761209
* ScalarTypeExtension :
11771210
* - extend scalar Name Directives[Const]

0 commit comments

Comments
 (0)