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(' | '); +}