From 7a93bba8265805996c3cf8187b52a362ccc5bc40 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Wed, 18 Apr 2018 18:29:27 -0400 Subject: [PATCH 1/5] 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: From e888d2132980d97cf42787ae35f983755aabf261 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Wed, 18 Apr 2018 20:23:26 -0400 Subject: [PATCH 2/5] Support extendSchema() --- src/language/ast.js | 6 +- src/type/schema.js | 8 +- src/utilities/__tests__/extendSchema-test.js | 178 +++++++++++++++++++ src/utilities/extendSchema.js | 63 +++++-- 4 files changed, 234 insertions(+), 21 deletions(-) diff --git a/src/language/ast.js b/src/language/ast.js index 027574bdb0..8a1a527fd4 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -396,7 +396,7 @@ export type TypeSystemDefinitionNode = export type SchemaDefinitionNode = { +kind: 'SchemaDefinition', +loc?: Location, - +directives: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +operationTypes: $ReadOnlyArray, }; @@ -517,8 +517,8 @@ export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode; export type SchemaExtensionNode = { +kind: 'SchemaExtension', +loc?: Location, - +directives: $ReadOnlyArray, - +operationTypes: $ReadOnlyArray, + +directives?: $ReadOnlyArray, + +operationTypes?: $ReadOnlyArray, }; // Type Extensions diff --git a/src/type/schema.js b/src/type/schema.js index 4a6f917890..96ac0167b3 100644 --- a/src/type/schema.js +++ b/src/type/schema.js @@ -21,7 +21,10 @@ import type { GraphQLAbstractType, GraphQLObjectType, } from './definition'; -import type { SchemaDefinitionNode } from '../language/ast'; +import type { + SchemaDefinitionNode, + SchemaExtensionNode, +} from '../language/ast'; import { GraphQLDirective, isDirective, @@ -73,6 +76,7 @@ export function isSchema(schema) { */ export class GraphQLSchema { astNode: ?SchemaDefinitionNode; + extensionASTNodes: ?$ReadOnlyArray; _queryType: ?GraphQLObjectType; _mutationType: ?GraphQLObjectType; _subscriptionType: ?GraphQLObjectType; @@ -120,6 +124,7 @@ export class GraphQLSchema { // Provide specified directives (e.g. @include and @skip) by default. this._directives = config.directives || specifiedDirectives; this.astNode = config.astNode; + this.extensionASTNodes = config.extensionASTNodes; // Build type map now to detect any errors within this schema. let initialTypes: Array = [ @@ -255,6 +260,7 @@ export type GraphQLSchemaConfig = { types?: ?Array, directives?: ?Array, astNode?: ?SchemaDefinitionNode, + extensionASTNodes?: ?$ReadOnlyArray, ...GraphQLSchemaValidationOptions, }; diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 857d93b593..05740c6a35 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -876,4 +876,182 @@ describe('extendSchema', () => { ); }); }); + + describe('can add additional root operation types', () => { + it('does not automatically include common root type names', () => { + const ast = parse(` + type Mutation { + doSomething: String + } + `); + const schema = extendSchema(testSchema, ast); + expect(schema.getMutationType()).to.equal(null); + }); + + it('does not allow new schema within an extension', () => { + const ast = parse(` + schema { + mutation: Mutation + } + + type Mutation { + doSomething: String + } + `); + expect(() => extendSchema(testSchema, ast)).to.throw( + 'Cannot define a new schema within a schema extension.', + ); + }); + + it('adds new root types via schema extension', () => { + const ast = parse(` + extend schema { + mutation: Mutation + } + + type Mutation { + doSomething: String + } + `); + const schema = extendSchema(testSchema, ast); + const mutationType = schema.getMutationType(); + expect(mutationType && mutationType.name).to.equal('Mutation'); + }); + + it('adds multiple new root types via schema extension', () => { + const ast = parse(` + extend schema { + mutation: Mutation + subscription: Subscription + } + + type Mutation { + doSomething: String + } + + type Subscription { + hearSomething: String + } + `); + const schema = extendSchema(testSchema, ast); + const mutationType = schema.getMutationType(); + const subscriptionType = schema.getSubscriptionType(); + expect(mutationType && mutationType.name).to.equal('Mutation'); + expect(subscriptionType && subscriptionType.name).to.equal( + 'Subscription', + ); + }); + + it('applies multiple schema extensions', () => { + const ast = parse(` + extend schema { + mutation: Mutation + } + + extend schema { + subscription: Subscription + } + + type Mutation { + doSomething: String + } + + type Subscription { + hearSomething: String + } + `); + const schema = extendSchema(testSchema, ast); + const mutationType = schema.getMutationType(); + const subscriptionType = schema.getSubscriptionType(); + expect(mutationType && mutationType.name).to.equal('Mutation'); + expect(subscriptionType && subscriptionType.name).to.equal( + 'Subscription', + ); + }); + + it('schema extension AST are available from schema object', () => { + const ast = parse(` + extend schema { + mutation: Mutation + } + + extend schema { + subscription: Subscription + } + + type Mutation { + doSomething: String + } + + type Subscription { + hearSomething: String + } + `); + const schema = extendSchema(testSchema, ast); + expect(schema.extensionASTNodes.map(print).join('\n')).to.equal(dedent` + extend schema { + mutation: Mutation + } + extend schema { + subscription: Subscription + }`); + }); + + it('does not allow redefining an existing root type', () => { + const ast = parse(` + extend schema { + query: SomeType + } + + type SomeType { + seeSomething: String + } + `); + expect(() => extendSchema(testSchema, ast)).to.throw( + 'Must provide only one query type in schema.', + ); + }); + + it('does not allow defining a root operation type twice', () => { + const ast = parse(` + extend schema { + mutation: Mutation + } + + extend schema { + mutation: Mutation + } + + type Mutation { + doSomething: String + } + `); + expect(() => extendSchema(testSchema, ast)).to.throw( + 'Must provide only one mutation type in schema.', + ); + }); + + it('does not allow defining a root operation type with different types', () => { + const ast = parse(` + extend schema { + mutation: Mutation + } + + extend schema { + mutation: SomethingElse + } + + type Mutation { + doSomething: String + } + + type SomethingElse { + doSomethingElse: String + } + `); + expect(() => extendSchema(testSchema, ast)).to.throw( + 'Must provide only one mutation type in schema.', + ); + }); + }); }); diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index cab1321acb..49fe0fab25 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -35,7 +35,11 @@ import { GraphQLDirective } from '../type/directives'; import { Kind } from '../language/kinds'; import type { GraphQLType, GraphQLNamedType } from '../type/definition'; -import type { DocumentNode, DirectiveDefinitionNode } from '../language/ast'; +import type { + DocumentNode, + DirectiveDefinitionNode, + SchemaExtensionNode, +} from '../language/ast'; type Options = {| ...GraphQLSchemaValidationOptions, @@ -88,9 +92,21 @@ export function extendSchema( // have the same name. For example, a type named "skip". const directiveDefinitions: Array = []; + // Schema extensions are collected which may add additional operation types. + const schemaExtensions: Array = []; + for (let i = 0; i < documentAST.definitions.length; i++) { const def = documentAST.definitions[i]; switch (def.kind) { + case Kind.SCHEMA_DEFINITION: + // Sanity check that a schema extension is not defining a new schema + throw new GraphQLError( + 'Cannot define a new schema within a schema extension.', + [def], + ); + case Kind.SCHEMA_EXTENSION: + schemaExtensions.push(def); + break; case Kind.OBJECT_TYPE_DEFINITION: case Kind.INTERFACE_TYPE_DEFINITION: case Kind.ENUM_TYPE_DEFINITION: @@ -156,7 +172,8 @@ export function extendSchema( if ( Object.keys(typeExtensionsMap).length === 0 && Object.keys(typeDefinitionMap).length === 0 && - directiveDefinitions.length === 0 + directiveDefinitions.length === 0 && + schemaExtensions.length === 0 ) { return schema; } @@ -181,21 +198,32 @@ export function extendSchema( const extendTypeCache = Object.create(null); - // Get the root Query, Mutation, and Subscription object types. + // Get the extended root operation types. const existingQueryType = schema.getQueryType(); - const queryType = existingQueryType - ? getExtendedType(existingQueryType) - : null; - const existingMutationType = schema.getMutationType(); - const mutationType = existingMutationType - ? getExtendedType(existingMutationType) - : null; - const existingSubscriptionType = schema.getSubscriptionType(); - const subscriptionType = existingSubscriptionType - ? getExtendedType(existingSubscriptionType) - : null; + const operationTypes = { + query: existingQueryType ? getExtendedType(existingQueryType) : null, + mutation: existingMutationType + ? getExtendedType(existingMutationType) + : null, + subscription: existingSubscriptionType + ? getExtendedType(existingSubscriptionType) + : null, + }; + + // Then, incorporate all schema extensions. + schemaExtensions.forEach(schemaExtension => { + if (schemaExtension.operationTypes) { + schemaExtension.operationTypes.forEach(operationType => { + const operation = operationType.operation; + if (operationTypes[operation]) { + throw new Error(`Must provide only one ${operation} type in schema.`); + } + operationTypes[operation] = astBuilder.buildType(operationType.type); + }); + } + }); const types = [ // Iterate through all types, getting the type definition for each, ensuring @@ -215,12 +243,13 @@ export function extendSchema( // Then produce and return a Schema with these types. return new GraphQLSchema({ - query: queryType, - mutation: mutationType, - subscription: subscriptionType, + query: operationTypes.query, + mutation: operationTypes.mutation, + subscription: operationTypes.subscription, types, directives: getMergedDirectives(), astNode: schema.astNode, + extensionASTNodes: schemaExtensions, allowedLegacyNames, }); From 6e65e8e828f60e84ab8963be51a36652508c0c7d Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Wed, 18 Apr 2018 20:27:18 -0400 Subject: [PATCH 3/5] Formatting edits --- src/language/ast.js | 10 +++++----- src/language/kinds.js | 6 +++--- src/language/parser.js | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/language/ast.js b/src/language/ast.js index 8a1a527fd4..6325c25ef4 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -124,14 +124,14 @@ export type ASTNode = | EnumTypeDefinitionNode | EnumValueDefinitionNode | InputObjectTypeDefinitionNode + | DirectiveDefinitionNode | SchemaExtensionNode | ScalarTypeExtensionNode | ObjectTypeExtensionNode | InterfaceTypeExtensionNode | UnionTypeExtensionNode | EnumTypeExtensionNode - | InputObjectTypeExtensionNode - | DirectiveDefinitionNode; + | InputObjectTypeExtensionNode; /** * Utility type listing all nodes indexed by their kind. @@ -172,6 +172,7 @@ export type ASTKindToNode = { EnumTypeDefinition: EnumTypeDefinitionNode, EnumValueDefinition: EnumValueDefinitionNode, InputObjectTypeDefinition: InputObjectTypeDefinitionNode, + DirectiveDefinition: DirectiveDefinitionNode, SchemaExtension: SchemaExtensionNode, ScalarTypeExtension: ScalarTypeExtensionNode, ObjectTypeExtension: ObjectTypeExtensionNode, @@ -179,7 +180,6 @@ export type ASTKindToNode = { UnionTypeExtension: UnionTypeExtensionNode, EnumTypeExtension: EnumTypeExtensionNode, InputObjectTypeExtension: InputObjectTypeExtensionNode, - DirectiveDefinition: DirectiveDefinitionNode, }; // Name @@ -390,8 +390,8 @@ export type NonNullTypeNode = { export type TypeSystemDefinitionNode = | SchemaDefinitionNode | TypeDefinitionNode - | TypeSystemExtensionNode - | DirectiveDefinitionNode; + | DirectiveDefinitionNode + | TypeSystemExtensionNode; export type SchemaDefinitionNode = { +kind: 'SchemaDefinition', diff --git a/src/language/kinds.js b/src/language/kinds.js index 40f40b1ffa..c6f1587ee4 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', + // Directive Definitions + DIRECTIVE_DEFINITION: 'DirectiveDefinition', + // Type System Extensions SCHEMA_EXTENSION: 'SchemaExtension', @@ -72,9 +75,6 @@ export const Kind = Object.freeze({ UNION_TYPE_EXTENSION: 'UnionTypeExtension', ENUM_TYPE_EXTENSION: 'EnumTypeExtension', INPUT_OBJECT_TYPE_EXTENSION: 'InputObjectTypeExtension', - - // Directive Definitions - DIRECTIVE_DEFINITION: 'DirectiveDefinition', }); /** diff --git a/src/language/parser.js b/src/language/parser.js index bc2c23b3bf..366233ca38 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -752,10 +752,10 @@ export function parseNamedType(lexer: Lexer<*>): NamedTypeNode { /** * TypeSystemDefinition : - * - TypeSystemExtension * - SchemaDefinition * - TypeDefinition * - DirectiveDefinition + * - TypeSystemExtension * * TypeDefinition : * - ScalarTypeDefinition @@ -785,10 +785,10 @@ function parseTypeSystemDefinition(lexer: Lexer<*>): TypeSystemDefinitionNode { return parseEnumTypeDefinition(lexer); case 'input': return parseInputObjectTypeDefinition(lexer); - case 'extend': - return parseTypeSystemExtension(lexer); case 'directive': return parseDirectiveDefinition(lexer); + case 'extend': + return parseTypeSystemExtension(lexer); } } From ff5f7440af4cee262154b939aedde6e8476ba283 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Wed, 18 Apr 2018 20:56:59 -0400 Subject: [PATCH 4/5] Add parsing and validation tests --- src/language/__tests__/schema-parser-test.js | 60 +++++++++++++++++++ src/language/ast.js | 8 +-- src/language/parser.js | 2 +- src/language/printer.js | 22 +++---- src/language/visitor.js | 7 ++- .../__tests__/ExecutableDefinitions-test.js | 3 + .../__tests__/KnownDirectives-test.js | 5 ++ 7 files changed, 88 insertions(+), 19 deletions(-) diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index 1c9fe0f144..c35e99a4f3 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -263,6 +263,66 @@ extend type Hello { ); }); + it('Schema extension', () => { + const body = ` + extend schema { + mutation: Mutation + }`; + const doc = parse(body); + const expected = { + kind: 'Document', + definitions: [ + { + kind: 'SchemaExtension', + directives: [], + operationTypes: [ + { + kind: 'OperationTypeDefinition', + operation: 'mutation', + type: typeNode('Mutation', { start: 41, end: 49 }), + loc: { start: 31, end: 49 }, + }, + ], + loc: { start: 7, end: 57 }, + }, + ], + loc: { start: 0, end: 57 }, + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + + it('Schema extension with only directives', () => { + const body = 'extend schema @directive'; + const doc = parse(body); + const expected = { + kind: 'Document', + definitions: [ + { + kind: 'SchemaExtension', + directives: [ + { + kind: 'Directive', + name: nameNode('directive', { start: 15, end: 24 }), + arguments: [], + loc: { start: 14, end: 24 }, + }, + ], + operationTypes: [], + loc: { start: 0, end: 24 }, + }, + ], + loc: { start: 0, end: 24 }, + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + + it('Schema extension without anything throws', () => { + expectSyntaxError('extend schema', 'Unexpected ', { + line: 1, + column: 14, + }); + }); + it('Simple non-null type', () => { const body = ` type Hello { diff --git a/src/language/ast.js b/src/language/ast.js index 6325c25ef4..8fb3783929 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -503,11 +503,11 @@ export type InputObjectTypeDefinitionNode = { export type DirectiveDefinitionNode = { +kind: 'DirectiveDefinition', - +loc ?: Location, - +description ?: StringValueNode, + +loc?: Location, + +description?: StringValueNode, +name: NameNode, - +arguments ?: $ReadOnlyArray < InputValueDefinitionNode >, - +locations: $ReadOnlyArray < NameNode >, + +arguments?: $ReadOnlyArray, + +locations: $ReadOnlyArray, }; // Type System Extensions diff --git a/src/language/parser.js b/src/language/parser.js index 366233ca38..ff2b60329c 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -52,6 +52,7 @@ import type { EnumTypeDefinitionNode, EnumValueDefinitionNode, InputObjectTypeDefinitionNode, + DirectiveDefinitionNode, TypeSystemExtensionNode, SchemaExtensionNode, ScalarTypeExtensionNode, @@ -60,7 +61,6 @@ import type { UnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, - DirectiveDefinitionNode, } from './ast'; import { Kind } from './kinds'; diff --git a/src/language/printer.js b/src/language/printer.js index 262eede320..0242898f52 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -175,6 +175,17 @@ const printDocASTReducer = { join(['input', name, join(directives, ' '), block(fields)], ' '), ), + DirectiveDefinition: addDescription( + ({ name, arguments: args, locations }) => + 'directive @' + + name + + (args.every(arg => arg.indexOf('\n') === -1) + ? wrap('(', join(args, ', '), ')') + : wrap('(\n', indent(join(args, '\n')), '\n)')) + + ' on ' + + join(locations, ' | '), + ), + SchemaExtension: ({ directives, operationTypes }) => join(['extend schema', join(directives, ' '), block(operationTypes)], ' '), @@ -212,17 +223,6 @@ const printDocASTReducer = { InputObjectTypeExtension: ({ name, directives, fields }) => join(['extend input', name, join(directives, ' '), block(fields)], ' '), - - DirectiveDefinition: addDescription( - ({ name, arguments: args, locations }) => - 'directive @' + - name + - (args.every(arg => arg.indexOf('\n') === -1) - ? wrap('(', join(args, ', '), ')') - : wrap('(\n', indent(join(args, '\n')), '\n)')) + - ' on ' + - join(locations, ' | '), - ), }; function addDescription(cb) { diff --git a/src/language/visitor.js b/src/language/visitor.js index c170a5c8ec..00e4510992 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -99,7 +99,6 @@ export const QueryDocumentKeys = { NonNullType: ['type'], SchemaDefinition: ['directives', 'operationTypes'], - SchemaExtension: ['directives', 'operationTypes'], OperationTypeDefinition: ['type'], ScalarTypeDefinition: ['description', 'name', 'directives'], @@ -124,14 +123,16 @@ export const QueryDocumentKeys = { EnumValueDefinition: ['description', 'name', 'directives'], InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'], + DirectiveDefinition: ['description', 'name', 'arguments', 'locations'], + + SchemaExtension: ['directives', 'operationTypes'], + ScalarTypeExtension: ['name', 'directives'], ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'], InterfaceTypeExtension: ['name', 'directives', 'fields'], UnionTypeExtension: ['name', 'directives', 'types'], EnumTypeExtension: ['name', 'directives', 'values'], InputObjectTypeExtension: ['name', 'directives', 'fields'], - - DirectiveDefinition: ['description', 'name', 'arguments', 'locations'], }; export const BREAK = {}; diff --git a/src/validation/__tests__/ExecutableDefinitions-test.js b/src/validation/__tests__/ExecutableDefinitions-test.js index 3a626a3143..922420bb32 100644 --- a/src/validation/__tests__/ExecutableDefinitions-test.js +++ b/src/validation/__tests__/ExecutableDefinitions-test.js @@ -87,10 +87,13 @@ describe('Validate: Executable definitions', () => { type Query { test: String } + + extend schema @directive `, [ nonExecutableDefinition('schema', 2, 7), nonExecutableDefinition('Query', 6, 7), + nonExecutableDefinition('schema', 10, 7), ], ); }); diff --git a/src/validation/__tests__/KnownDirectives-test.js b/src/validation/__tests__/KnownDirectives-test.js index ae46029650..0e0daf5641 100644 --- a/src/validation/__tests__/KnownDirectives-test.js +++ b/src/validation/__tests__/KnownDirectives-test.js @@ -178,6 +178,8 @@ describe('Validate: Known directives', () => { schema @onSchema { query: MyQuery } + + extend schema @onSchema `, ); }); @@ -209,6 +211,8 @@ describe('Validate: Known directives', () => { schema @onObject { query: MyQuery } + + extend schema @onObject `, [ misplacedDirective('onInterface', 'OBJECT', 2, 43), @@ -249,6 +253,7 @@ describe('Validate: Known directives', () => { 24, ), misplacedDirective('onObject', 'SCHEMA', 22, 16), + misplacedDirective('onObject', 'SCHEMA', 26, 23), ], ); }); From abdffeb7c783ba62357fad85cadde21737b8a73a Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Mon, 23 Apr 2018 15:24:26 -0400 Subject: [PATCH 5/5] Adjust grammar rules to match spec definitions --- src/language/ast.js | 6 +++--- src/language/parser.js | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/language/ast.js b/src/language/ast.js index 8fb3783929..000bc3bdad 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -200,7 +200,8 @@ export type DocumentNode = { export type DefinitionNode = | ExecutableDefinitionNode - | TypeSystemDefinitionNode; // experimental non-spec addition. + | TypeSystemDefinitionNode + | TypeSystemExtensionNode; export type ExecutableDefinitionNode = | OperationDefinitionNode @@ -390,8 +391,7 @@ export type NonNullTypeNode = { export type TypeSystemDefinitionNode = | SchemaDefinitionNode | TypeDefinitionNode - | DirectiveDefinitionNode - | TypeSystemExtensionNode; + | DirectiveDefinitionNode; export type SchemaDefinitionNode = { +kind: 'SchemaDefinition', diff --git a/src/language/parser.js b/src/language/parser.js index ff2b60329c..6fe84fe9da 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -212,6 +212,7 @@ function parseDocument(lexer: Lexer<*>): DocumentNode { * Definition : * - ExecutableDefinition * - TypeSystemDefinition + * - TypeSystemExtension */ function parseDefinition(lexer: Lexer<*>): DefinitionNode { if (peek(lexer, TokenKind.NAME)) { @@ -228,15 +229,14 @@ function parseDefinition(lexer: Lexer<*>): DefinitionNode { case 'union': case 'enum': case 'input': - case 'extend': case 'directive': - // Note: The schema definition language is an experimental addition. return parseTypeSystemDefinition(lexer); + case 'extend': + return parseTypeSystemExtension(lexer); } } else if (peek(lexer, TokenKind.BRACE_L)) { return parseExecutableDefinition(lexer); } else if (peekDescription(lexer)) { - // Note: The schema definition language is an experimental addition. return parseTypeSystemDefinition(lexer); } @@ -755,7 +755,6 @@ export function parseNamedType(lexer: Lexer<*>): NamedTypeNode { * - SchemaDefinition * - TypeDefinition * - DirectiveDefinition - * - TypeSystemExtension * * TypeDefinition : * - ScalarTypeDefinition @@ -787,8 +786,6 @@ function parseTypeSystemDefinition(lexer: Lexer<*>): TypeSystemDefinitionNode { return parseInputObjectTypeDefinition(lexer); case 'directive': return parseDirectiveDefinition(lexer); - case 'extend': - return parseTypeSystemExtension(lexer); } }