From 397dffde0ba7139d275617b5436effe0ce21a3d1 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Thu, 17 Mar 2016 19:47:06 -0700 Subject: [PATCH] [RFC] Directives in schema language This adds directives to schema language and to the utilities that use it (schema parser, and buildASTSchema). Directives are one of the few missing pieces from representing a full schema in the schema language. Note: the schema language is still experimental, so there is no corresponding change to the spec yet. DirectiveDefinition : - directive @ Name ArgumentsDefinition? on DirectiveLocations DirectiveLocations : - Name - DirectiveLocations | Name Example: ``` directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT ``` --- .../__tests__/schema-kitchen-sink.graphql | 7 +++ src/language/__tests__/schema-printer.js | 5 ++ src/language/ast.js | 10 ++++ src/language/kinds.js | 7 +++ src/language/parser.js | 38 +++++++++++++ src/language/printer.js | 4 ++ src/language/visitor.js | 1 + src/type/directives.js | 53 ++++++++++++++----- src/utilities/__tests__/buildASTSchema.js | 12 +++++ src/utilities/__tests__/schemaPrinter.js | 4 ++ src/utilities/buildASTSchema.js | 20 +++++++ src/utilities/buildClientSchema.js | 2 +- src/utilities/schemaPrinter.js | 26 ++++++--- 13 files changed, 170 insertions(+), 19 deletions(-) diff --git a/src/language/__tests__/schema-kitchen-sink.graphql b/src/language/__tests__/schema-kitchen-sink.graphql index be875685df..7e4918d2b6 100644 --- a/src/language/__tests__/schema-kitchen-sink.graphql +++ b/src/language/__tests__/schema-kitchen-sink.graphql @@ -36,3 +36,10 @@ input InputType { extend type Foo { seven(argument: [String]): Type } + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include(if: Boolean!) + on FIELD + | FRAGMENT_SPREAD + | INLINE_FRAGMENT diff --git a/src/language/__tests__/schema-printer.js b/src/language/__tests__/schema-printer.js index 50af0b6d55..e2d6bad6f3 100644 --- a/src/language/__tests__/schema-printer.js +++ b/src/language/__tests__/schema-printer.js @@ -49,6 +49,7 @@ describe('Printer', () => { const printed = print(ast); + /* eslint-disable max-len */ expect(printed).to.equal( `type Foo implements Bar { one: Type @@ -81,6 +82,10 @@ input InputType { extend type Foo { seven(argument: [String]): Type } + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT `); }); diff --git a/src/language/ast.js b/src/language/ast.js index 90616bc496..f56da99462 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -57,6 +57,7 @@ export type Node = Name | EnumValueDefinition | InputObjectTypeDefinition | TypeExtensionDefinition + | DirectiveDefinition // Name @@ -78,6 +79,7 @@ export type Definition = OperationDefinition | FragmentDefinition | TypeDefinition | TypeExtensionDefinition + | DirectiveDefinition export type OperationDefinition = { kind: 'OperationDefinition'; @@ -332,3 +334,11 @@ export type TypeExtensionDefinition = { loc?: ?Location; definition: ObjectTypeDefinition; } + +export type DirectiveDefinition = { + kind: 'DirectiveDefinition'; + loc?: ?Location; + name: Name; + arguments?: ?Array; + locations: Array; +} diff --git a/src/language/kinds.js b/src/language/kinds.js index 41d28e8fa5..d586d89088 100644 --- a/src/language/kinds.js +++ b/src/language/kinds.js @@ -59,4 +59,11 @@ export const SCALAR_TYPE_DEFINITION = 'ScalarTypeDefinition'; export const ENUM_TYPE_DEFINITION = 'EnumTypeDefinition'; export const ENUM_VALUE_DEFINITION = 'EnumValueDefinition'; export const INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition'; + +// Type Extensions + export const TYPE_EXTENSION_DEFINITION = 'TypeExtensionDefinition'; + +// Directive Definitions + +export const DIRECTIVE_DEFINITION = 'DirectiveDefinition'; diff --git a/src/language/parser.js b/src/language/parser.js index 4aa2230cba..2c406c4e4c 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -51,6 +51,8 @@ import type { InputObjectTypeDefinition, TypeExtensionDefinition, + + DirectiveDefinition, } from './ast'; import { @@ -94,6 +96,8 @@ import { INPUT_OBJECT_TYPE_DEFINITION, TYPE_EXTENSION_DEFINITION, + + DIRECTIVE_DEFINITION, } from './kinds'; @@ -205,6 +209,7 @@ function parseDefinition(parser: Parser): Definition { case 'enum': case 'input': return parseTypeDefinition(parser); case 'extend': return parseTypeExtensionDefinition(parser); + case 'directive': return parseDirectiveDefinition(parser); } } @@ -898,6 +903,39 @@ function parseTypeExtensionDefinition(parser: Parser): TypeExtensionDefinition { }; } +/** + * DirectiveDefinition : + * - directive @ Name ArgumentsDefinition? on DirectiveLocations + */ +function parseDirectiveDefinition(parser: Parser): DirectiveDefinition { + const start = parser.token.start; + expectKeyword(parser, 'directive'); + expect(parser, TokenKind.AT); + const name = parseName(parser); + const args = parseArgumentDefs(parser); + expectKeyword(parser, 'on'); + const locations = parseDirectiveLocations(parser); + return { + kind: DIRECTIVE_DEFINITION, + name, + arguments: args, + locations, + loc: loc(parser, start) + }; +} + +/** + * DirectiveLocations : + * - Name + * - DirectiveLocations | Name + */ +function parseDirectiveLocations(parser: Parser): Array { + const locations = []; + do { + locations.push(parseName(parser)); + } while (skip(parser, TokenKind.PIPE)); + return locations; +} // Core parsing utility functions diff --git a/src/language/printer.js b/src/language/printer.js index 977dc1ea77..7edc5d7c6d 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -123,6 +123,10 @@ const printDocASTReducer = { `input ${name} ${block(fields)}`, TypeExtensionDefinition: ({ definition }) => `extend ${definition}`, + + DirectiveDefinition: ({ name, arguments: args, locations }) => + 'directive @' + name + wrap('(', join(args, ', '), ')') + + ' on ' + join(locations, ' | '), }; /** diff --git a/src/language/visitor.js b/src/language/visitor.js index a49813f64c..3874aa2b04 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -48,6 +48,7 @@ export const QueryDocumentKeys = { EnumValueDefinition: [ 'name' ], InputObjectTypeDefinition: [ 'name', 'fields' ], TypeExtensionDefinition: [ 'definition' ], + DirectiveDefinition: [ 'name', 'arguments', 'locations' ], }; export const BREAK = {}; diff --git a/src/type/directives.js b/src/type/directives.js index b2290604c0..e93adb9674 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -8,8 +8,11 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -import { GraphQLNonNull } from './definition'; -import type { GraphQLArgument } from './definition'; +import { isInputType, GraphQLNonNull } from './definition'; +import type { + GraphQLFieldConfigArgumentMap, + GraphQLArgument +} from './definition'; import { GraphQLBoolean } from './scalars'; import invariant from '../jsutils/invariant'; import { assertValidName } from '../utilities/assertValidName'; @@ -47,7 +50,31 @@ export class GraphQLDirective { this.name = config.name; this.description = config.description; this.locations = config.locations; - this.args = config.args || []; + + const args = config.args; + if (!args) { + this.args = []; + } else { + invariant( + !Array.isArray(args), + `@${config.name} args must be an object with argument names as keys.` + ); + this.args = Object.keys(args).map(argName => { + assertValidName(argName); + const arg = args[argName]; + invariant( + isInputType(arg.type), + `@${config.name}(${argName}:) argument type must be ` + + `Input Type but got: ${arg.type}.` + ); + return { + name: argName, + description: arg.description === undefined ? null : arg.description, + type: arg.type, + defaultValue: arg.defaultValue === undefined ? null : arg.defaultValue + }; + }); + } } } @@ -55,7 +82,7 @@ type GraphQLDirectiveConfig = { name: string; description?: ?string; locations: Array; - args?: ?Array; + args?: ?GraphQLFieldConfigArgumentMap; } /** @@ -71,11 +98,12 @@ export const GraphQLIncludeDirective = new GraphQLDirective({ DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT, ], - args: [ - { name: 'if', + args: { + if: { type: new GraphQLNonNull(GraphQLBoolean), - description: 'Included when true.' } - ], + description: 'Included when true.' + } + }, }); /** @@ -91,9 +119,10 @@ export const GraphQLSkipDirective = new GraphQLDirective({ DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT, ], - args: [ - { name: 'if', + args: { + if: { type: new GraphQLNonNull(GraphQLBoolean), - description: 'Skipped when true.' } - ], + description: 'Skipped when true.' + } + }, }); diff --git a/src/utilities/__tests__/buildASTSchema.js b/src/utilities/__tests__/buildASTSchema.js index 4f6aea1c6b..06e85a3104 100644 --- a/src/utilities/__tests__/buildASTSchema.js +++ b/src/utilities/__tests__/buildASTSchema.js @@ -41,6 +41,18 @@ type HelloScalars { expect(output).to.equal(body); }); + it('With directives', () => { + const body = ` +directive @foo(arg: Int) on FIELD + +type Hello { + str: String +} +`; + const output = cycleOutput(body, 'Hello'); + expect(output).to.equal(body); + }); + it('Type modifiers', () => { const body = ` type HelloScalars { diff --git a/src/utilities/__tests__/schemaPrinter.js b/src/utilities/__tests__/schemaPrinter.js index 290cbe83ff..de64fbd1ff 100644 --- a/src/utilities/__tests__/schemaPrinter.js +++ b/src/utilities/__tests__/schemaPrinter.js @@ -508,6 +508,10 @@ type Root { const Schema = new GraphQLSchema({ query: Root }); const output = '\n' + printIntrospectionSchema(Schema); const introspectionSchema = ` +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + type __Directive { name: String! description: String diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index fd74a56c0f..0d309c4a9a 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -25,6 +25,7 @@ import { UNION_TYPE_DEFINITION, SCALAR_TYPE_DEFINITION, INPUT_OBJECT_TYPE_DEFINITION, + DIRECTIVE_DEFINITION, } from '../language/kinds'; import type { @@ -39,6 +40,7 @@ import type { ScalarTypeDefinition, EnumTypeDefinition, InputObjectTypeDefinition, + DirectiveDefinition, } from '../language/ast'; import { @@ -58,6 +60,8 @@ import { GraphQLNonNull, } from '../type'; +import { GraphQLDirective } from '../type/directives'; + import type { GraphQLType, GraphQLNamedType @@ -115,6 +119,7 @@ export function buildASTSchema( } const typeDefs: Array = []; + const directiveDefs: Array = []; for (let i = 0; i < ast.definitions.length; i++) { const d = ast.definitions[i]; switch (d.kind) { @@ -125,6 +130,10 @@ export function buildASTSchema( case SCALAR_TYPE_DEFINITION: case INPUT_OBJECT_TYPE_DEFINITION: typeDefs.push(d); + break; + case DIRECTIVE_DEFINITION: + directiveDefs.push(d); + break; } } @@ -160,13 +169,24 @@ export function buildASTSchema( typeDefs.forEach(def => typeDefNamed(def.name.value)); + const directives = directiveDefs.map(getDirective); + return new GraphQLSchema({ + directives, query: getObjectType(astMap[queryTypeName]), mutation: mutationTypeName ? getObjectType(astMap[mutationTypeName]) : null, subscription: subscriptionTypeName ? getObjectType(astMap[subscriptionTypeName]) : null, }); + function getDirective(directiveAST: DirectiveDefinition): GraphQLDirective { + return new GraphQLDirective({ + name: directiveAST.name.value, + locations: directiveAST.locations.map(node => node.value), + args: makeInputValues(directiveAST.arguments), + }); + } + function getObjectType(typeAST: TypeDefinition): GraphQLObjectType { const type = typeDefNamed(typeAST.name.value); invariant( diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index 93a07c6697..cb85252b83 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -342,7 +342,7 @@ export function buildClientSchema( name: directiveIntrospection.name, description: directiveIntrospection.description, locations, - args: directiveIntrospection.args.map(buildInputValue), + args: buildInputValueDefMap(directiveIntrospection.args), }); } diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index 854e805f31..3c36eddcf8 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -25,11 +25,15 @@ import { export function printSchema(schema: GraphQLSchema): string { - return printFilteredSchema(schema, isDefinedType); + return printFilteredSchema(schema, n => !isSpecDirective(n), isDefinedType); } export function printIntrospectionSchema(schema: GraphQLSchema): string { - return printFilteredSchema(schema, isIntrospectionType); + return printFilteredSchema(schema, isSpecDirective, isIntrospectionType); +} + +function isSpecDirective(directiveName: string): boolean { + return directiveName === 'skip' || directiveName === 'include'; } function isDefinedType(typename: string): boolean { @@ -52,14 +56,19 @@ function isBuiltInScalar(typename: string): boolean { function printFilteredSchema( schema: GraphQLSchema, + directiveFilter: (type: string) => boolean, typeFilter: (type: string) => boolean ): string { + const directives = schema.getDirectives() + .filter(directive => directiveFilter(directive.name)); const typeMap = schema.getTypeMap(); const types = Object.keys(typeMap) .filter(typeFilter) .sort((name1, name2) => name1.localeCompare(name2)) .map(typeName => typeMap[typeName]); - return types.map(printType).join('\n\n') + '\n'; + return directives.map(printDirective).concat( + types.map(printType) + ).join('\n\n') + '\n'; } function printType(type: GraphQLType): string { @@ -124,11 +133,11 @@ function printFields(type) { ).join('\n'); } -function printArgs(field) { - if (field.args.length === 0) { +function printArgs(fieldOrDirectives) { + if (fieldOrDirectives.args.length === 0) { return ''; } - return '(' + field.args.map(printInputValue).join(', ') + ')'; + return '(' + fieldOrDirectives.args.map(printInputValue).join(', ') + ')'; } function printInputValue(arg) { @@ -138,3 +147,8 @@ function printInputValue(arg) { } return argDecl; } + +function printDirective(directive) { + return 'directive @' + directive.name + printArgs(directive) + + ' on ' + directive.locations.join(' | '); +}