From 7a93bba8265805996c3cf8187b52a362ccc5bc40 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Wed, 18 Apr 2018 18:29:27 -0400 Subject: [PATCH] RFC: SchemaExtension This adds support for https://github.com/facebook/graphql/pull/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. --- src/index.js | 4 +- .../__tests__/schema-kitchen-sink.graphql | 6 +++ src/language/__tests__/schema-printer-test.js | 6 +++ src/language/ast.js | 37 +++++++++++----- src/language/index.js | 4 +- src/language/kinds.js | 3 ++ src/language/parser.js | 44 +++++++++++++++++-- src/language/printer.js | 3 ++ src/language/visitor.js | 1 + src/validation/rules/ExecutableDefinitions.js | 3 +- src/validation/rules/KnownDirectives.js | 1 + 11 files changed, 93 insertions(+), 19 deletions(-) diff --git a/src/index.js b/src/index.js index a3d2f7bc80..c439340a0a 100644 --- a/src/index.js +++ b/src/index.js @@ -245,6 +245,9 @@ export type { EnumTypeDefinitionNode, EnumValueDefinitionNode, InputObjectTypeDefinitionNode, + DirectiveDefinitionNode, + TypeSystemExtensionNode, + SchemaExtensionNode, TypeExtensionNode, ScalarTypeExtensionNode, ObjectTypeExtensionNode, @@ -252,7 +255,6 @@ export type { UnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, - DirectiveDefinitionNode, KindEnum, TokenKindEnum, DirectiveLocationEnum, diff --git a/src/language/__tests__/schema-kitchen-sink.graphql b/src/language/__tests__/schema-kitchen-sink.graphql index 5ce4ca6a06..1c7b5c3b30 100644 --- a/src/language/__tests__/schema-kitchen-sink.graphql +++ b/src/language/__tests__/schema-kitchen-sink.graphql @@ -123,3 +123,9 @@ directive @include2(if: Boolean!) on | FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +extend schema @onSchema + +extend schema @onSchema { + subscription: SubscriptionType +} diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index 00dd5d70b3..3803bd59cc 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -161,6 +161,12 @@ describe('Printer: SDL document', () => { directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + extend schema @onSchema + + extend schema @onSchema { + subscription: SubscriptionType + } `); }); }); diff --git a/src/language/ast.js b/src/language/ast.js index 6b6f5fbcd2..027574bdb0 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -124,6 +124,7 @@ export type ASTNode = | EnumTypeDefinitionNode | EnumValueDefinitionNode | InputObjectTypeDefinitionNode + | SchemaExtensionNode | ScalarTypeExtensionNode | ObjectTypeExtensionNode | InterfaceTypeExtensionNode @@ -171,6 +172,7 @@ export type ASTKindToNode = { EnumTypeDefinition: EnumTypeDefinitionNode, EnumValueDefinition: EnumValueDefinitionNode, InputObjectTypeDefinition: InputObjectTypeDefinitionNode, + SchemaExtension: SchemaExtensionNode, ScalarTypeExtension: ScalarTypeExtensionNode, ObjectTypeExtension: ObjectTypeExtensionNode, InterfaceTypeExtension: InterfaceTypeExtensionNode, @@ -388,7 +390,7 @@ export type NonNullTypeNode = { export type TypeSystemDefinitionNode = | SchemaDefinitionNode | TypeDefinitionNode - | TypeExtensionNode + | TypeSystemExtensionNode | DirectiveDefinitionNode; export type SchemaDefinitionNode = { @@ -497,6 +499,28 @@ export type InputObjectTypeDefinitionNode = { +fields?: $ReadOnlyArray, }; +// Directive Definitions + +export type DirectiveDefinitionNode = { + +kind: 'DirectiveDefinition', + +loc ?: Location, + +description ?: StringValueNode, + +name: NameNode, + +arguments ?: $ReadOnlyArray < InputValueDefinitionNode >, + +locations: $ReadOnlyArray < NameNode >, +}; + +// Type System Extensions + +export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode; + +export type SchemaExtensionNode = { + +kind: 'SchemaExtension', + +loc?: Location, + +directives: $ReadOnlyArray, + +operationTypes: $ReadOnlyArray, +}; + // Type Extensions export type TypeExtensionNode = @@ -554,14 +578,3 @@ export type InputObjectTypeExtensionNode = { +directives?: $ReadOnlyArray, +fields?: $ReadOnlyArray, }; - -// Directive Definitions - -export type DirectiveDefinitionNode = { - +kind: 'DirectiveDefinition', - +loc?: Location, - +description?: StringValueNode, - +name: NameNode, - +arguments?: $ReadOnlyArray, - +locations: $ReadOnlyArray, -}; diff --git a/src/language/index.js b/src/language/index.js index 874eedf9e7..e49e8917d1 100644 --- a/src/language/index.js +++ b/src/language/index.js @@ -76,6 +76,9 @@ export type { EnumTypeDefinitionNode, EnumValueDefinitionNode, InputObjectTypeDefinitionNode, + DirectiveDefinitionNode, + TypeSystemExtensionNode, + SchemaExtensionNode, TypeExtensionNode, ScalarTypeExtensionNode, ObjectTypeExtensionNode, @@ -83,7 +86,6 @@ export type { UnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, - DirectiveDefinitionNode, } from './ast'; export { DirectiveLocation } from './directiveLocation'; diff --git a/src/language/kinds.js b/src/language/kinds.js index d67f748e34..40f40b1ffa 100644 --- a/src/language/kinds.js +++ b/src/language/kinds.js @@ -62,6 +62,9 @@ export const Kind = Object.freeze({ ENUM_VALUE_DEFINITION: 'EnumValueDefinition', INPUT_OBJECT_TYPE_DEFINITION: 'InputObjectTypeDefinition', + // Type System Extensions + SCHEMA_EXTENSION: 'SchemaExtension', + // Type Extensions SCALAR_TYPE_EXTENSION: 'ScalarTypeExtension', OBJECT_TYPE_EXTENSION: 'ObjectTypeExtension', diff --git a/src/language/parser.js b/src/language/parser.js index 20120eeb67..bc2c23b3bf 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -52,7 +52,8 @@ import type { EnumTypeDefinitionNode, EnumValueDefinitionNode, InputObjectTypeDefinitionNode, - TypeExtensionNode, + TypeSystemExtensionNode, + SchemaExtensionNode, ScalarTypeExtensionNode, ObjectTypeExtensionNode, InterfaceTypeExtensionNode, @@ -751,9 +752,9 @@ export function parseNamedType(lexer: Lexer<*>): NamedTypeNode { /** * TypeSystemDefinition : + * - TypeSystemExtension * - SchemaDefinition * - TypeDefinition - * - TypeExtension * - DirectiveDefinition * * TypeDefinition : @@ -785,7 +786,7 @@ function parseTypeSystemDefinition(lexer: Lexer<*>): TypeSystemDefinitionNode { case 'input': return parseInputObjectTypeDefinition(lexer); case 'extend': - return parseTypeExtension(lexer); + return parseTypeSystemExtension(lexer); case 'directive': return parseDirectiveDefinition(lexer); } @@ -1141,6 +1142,10 @@ function parseInputFieldsDefinition( } /** + * TypeSystemExtension : + * - SchemaExtension + * - TypeExtension + * * TypeExtension : * - ScalarTypeExtension * - ObjectTypeExtension @@ -1149,11 +1154,13 @@ function parseInputFieldsDefinition( * - EnumTypeExtension * - InputObjectTypeDefinition */ -function parseTypeExtension(lexer: Lexer<*>): TypeExtensionNode { +function parseTypeSystemExtension(lexer: Lexer<*>): TypeSystemExtensionNode { const keywordToken = lexer.lookahead(); if (keywordToken.kind === TokenKind.NAME) { switch (keywordToken.value) { + case 'schema': + return parseSchemaExtension(lexer); case 'scalar': return parseScalarTypeExtension(lexer); case 'type': @@ -1172,6 +1179,35 @@ function parseTypeExtension(lexer: Lexer<*>): TypeExtensionNode { throw unexpected(lexer, keywordToken); } +/** + * SchemaExtension : + * - extend schema Directives[Const]? { OperationTypeDefinition+ } + * - extend schema Directives[Const] + */ +function parseSchemaExtension(lexer: Lexer<*>): SchemaExtensionNode { + const start = lexer.token; + expectKeyword(lexer, 'extend'); + expectKeyword(lexer, 'schema'); + const directives = parseDirectives(lexer, true); + const operationTypes = peek(lexer, TokenKind.BRACE_L) + ? many( + lexer, + TokenKind.BRACE_L, + parseOperationTypeDefinition, + TokenKind.BRACE_R, + ) + : []; + if (directives.length === 0 && operationTypes.length === 0) { + throw unexpected(lexer); + } + return { + kind: Kind.SCHEMA_EXTENSION, + directives, + operationTypes, + loc: loc(lexer, start), + }; +} + /** * ScalarTypeExtension : * - extend scalar Name Directives[Const] diff --git a/src/language/printer.js b/src/language/printer.js index 558b0f7e0e..262eede320 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -175,6 +175,9 @@ const printDocASTReducer = { join(['input', name, join(directives, ' '), block(fields)], ' '), ), + SchemaExtension: ({ directives, operationTypes }) => + join(['extend schema', join(directives, ' '), block(operationTypes)], ' '), + ScalarTypeExtension: ({ name, directives }) => join(['extend scalar', name, join(directives, ' ')], ' '), diff --git a/src/language/visitor.js b/src/language/visitor.js index ef166e3726..c170a5c8ec 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -99,6 +99,7 @@ export const QueryDocumentKeys = { NonNullType: ['type'], SchemaDefinition: ['directives', 'operationTypes'], + SchemaExtension: ['directives', 'operationTypes'], OperationTypeDefinition: ['type'], ScalarTypeDefinition: ['description', 'name', 'directives'], diff --git a/src/validation/rules/ExecutableDefinitions.js b/src/validation/rules/ExecutableDefinitions.js index b7b2d9a382..ac6a5a4bb9 100644 --- a/src/validation/rules/ExecutableDefinitions.js +++ b/src/validation/rules/ExecutableDefinitions.js @@ -33,7 +33,8 @@ export function ExecutableDefinitions(context: ValidationContext): ASTVisitor { context.reportError( new GraphQLError( nonExecutableDefinitionMessage( - definition.kind === Kind.SCHEMA_DEFINITION + definition.kind === Kind.SCHEMA_DEFINITION || + definition.kind === Kind.SCHEMA_EXTENSION ? 'schema' : definition.name.value, ), diff --git a/src/validation/rules/KnownDirectives.js b/src/validation/rules/KnownDirectives.js index 9972ce8acf..05c12f39f6 100644 --- a/src/validation/rules/KnownDirectives.js +++ b/src/validation/rules/KnownDirectives.js @@ -83,6 +83,7 @@ function getDirectiveLocationForASTPath(ancestors) { case Kind.FRAGMENT_DEFINITION: return DirectiveLocation.FRAGMENT_DEFINITION; case Kind.SCHEMA_DEFINITION: + case Kind.SCHEMA_EXTENSION: return DirectiveLocation.SCHEMA; case Kind.SCALAR_TYPE_DEFINITION: case Kind.SCALAR_TYPE_EXTENSION: