diff --git a/src/language/__tests__/schema-kitchen-sink.graphql b/src/language/__tests__/schema-kitchen-sink.graphql index c3497d08ed..d148276253 100644 --- a/src/language/__tests__/schema-kitchen-sink.graphql +++ b/src/language/__tests__/schema-kitchen-sink.graphql @@ -19,29 +19,52 @@ type Foo implements Bar { six(argument: InputType = {key: "value"}): Type } +type AnnotatedObject @onObject(arg: "value") { + annotatedField(arg: Type = "default" @onArg): Type @onField +} + interface Bar { one: Type four(argument: String = "string"): String } +interface AnnotatedInterface @onInterface { + annotatedField(arg: Type @onArg): Type @onField +} + union Feed = Story | Article | Advert +union AnnotatedUnion @onUnion = A | B + scalar CustomScalar +scalar AnnotatedScalar @onScalar + enum Site { DESKTOP MOBILE } +enum AnnotatedEnum @onEnum { + ANNOTATED_VALUE @onEnumValue + OTHER_VALUE +} + input InputType { key: String! answer: Int = 42 } +input AnnotatedInput @onInputObjectType { + annotatedField: Type @onField +} + extend type Foo { seven(argument: [String]): Type } +extend type Foo @onType {} + type NoFields {} directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index dea11e574b..6168cf42d5 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -52,6 +52,7 @@ function fieldNodeWithArgs(name, type, args, loc) { name, arguments: args, type, + directives: [], loc, }; } @@ -60,6 +61,7 @@ function enumValueNode(name, loc) { return { kind: 'EnumValueDefinition', name: nameNode(name, loc), + directives: [], loc, }; } @@ -70,6 +72,7 @@ function inputValueNode(name, type, defaultValue, loc) { name, type, defaultValue, + directives: [], loc, }; } @@ -89,6 +92,7 @@ type Hello { kind: 'ObjectTypeDefinition', name: nameNode('Hello', loc(6, 11)), interfaces: [], + directives: [], fields: [ fieldNode( nameNode('world', loc(16, 21)), @@ -120,6 +124,7 @@ extend type Hello { kind: 'ObjectTypeDefinition', name: nameNode('Hello', loc(13, 18)), interfaces: [], + directives: [], fields: [ fieldNode( nameNode('world', loc(23, 28)), @@ -151,6 +156,7 @@ type Hello { kind: 'ObjectTypeDefinition', name: nameNode('Hello', loc(6, 11)), interfaces: [], + directives: [], fields: [ fieldNode( nameNode('world', loc(16, 21)), @@ -182,6 +188,7 @@ type Hello { kind: 'ObjectTypeDefinition', name: nameNode('Hello', loc(5, 10)), interfaces: [ typeNode('World', loc(22, 27)) ], + directives: [], fields: [], loc: loc(0, 31), } @@ -205,6 +212,7 @@ type Hello { typeNode('Wo', loc(22, 24)), typeNode('rld', loc(26, 29)) ], + directives: [], fields: [], loc: loc(0, 33), } @@ -224,6 +232,7 @@ type Hello { { kind: 'EnumTypeDefinition', name: nameNode('Hello', loc(5, 10)), + directives: [], values: [ enumValueNode('WORLD', loc(13, 18)) ], loc: loc(0, 20), } @@ -243,6 +252,7 @@ type Hello { { kind: 'EnumTypeDefinition', name: nameNode('Hello', loc(5, 10)), + directives: [], values: [ enumValueNode('WO', loc(13, 15)), enumValueNode('RLD', loc(17, 20)), @@ -268,6 +278,7 @@ interface Hello { { kind: 'InterfaceTypeDefinition', name: nameNode('Hello', loc(11, 16)), + directives: [], fields: [ fieldNode( nameNode('world', loc(21, 26)), @@ -297,6 +308,7 @@ type Hello { kind: 'ObjectTypeDefinition', name: nameNode('Hello', loc(6, 11)), interfaces: [], + directives: [], fields: [ fieldNodeWithArgs( nameNode('world', loc(16, 21)), @@ -334,6 +346,7 @@ type Hello { kind: 'ObjectTypeDefinition', name: nameNode('Hello', loc(6, 11)), interfaces: [], + directives: [], fields: [ fieldNodeWithArgs( nameNode('world', loc(16, 21)), @@ -375,6 +388,7 @@ type Hello { kind: 'ObjectTypeDefinition', name: nameNode('Hello', loc(6, 11)), interfaces: [], + directives: [], fields: [ fieldNodeWithArgs( nameNode('world', loc(16, 21)), @@ -416,6 +430,7 @@ type Hello { kind: 'ObjectTypeDefinition', name: nameNode('Hello', loc(6, 11)), interfaces: [], + directives: [], fields: [ fieldNodeWithArgs( nameNode('world', loc(16, 21)), @@ -455,6 +470,7 @@ type Hello { { kind: 'UnionTypeDefinition', name: nameNode('Hello', loc(6, 11)), + directives: [], types: [ typeNode('World', loc(14, 19)) ], loc: loc(0, 19), } @@ -474,6 +490,7 @@ type Hello { { kind: 'UnionTypeDefinition', name: nameNode('Hello', loc(6, 11)), + directives: [], types: [ typeNode('Wo', loc(14, 16)), typeNode('Rld', loc(19, 22)), @@ -496,6 +513,7 @@ type Hello { { kind: 'ScalarTypeDefinition', name: nameNode('Hello', loc(7, 12)), + directives: [], loc: loc(0, 12), } ], @@ -517,6 +535,7 @@ input Hello { { kind: 'InputObjectTypeDefinition', name: nameNode('Hello', loc(7, 12)), + directives: [], fields: [ inputValueNode( nameNode('world', loc(17, 22)), diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index 53d691b526..0d36f219b8 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -65,29 +65,52 @@ type Foo implements Bar { six(argument: InputType = {key: "value"}): Type } +type AnnotatedObject @onObject(arg: "value") { + annotatedField(arg: Type = "default" @onArg): Type @onField +} + interface Bar { one: Type four(argument: String = "string"): String } +interface AnnotatedInterface @onInterface { + annotatedField(arg: Type @onArg): Type @onField +} + union Feed = Story | Article | Advert +union AnnotatedUnion @onUnion = A | B + scalar CustomScalar +scalar AnnotatedScalar @onScalar + enum Site { DESKTOP MOBILE } +enum AnnotatedEnum @onEnum { + ANNOTATED_VALUE @onEnumValue + OTHER_VALUE +} + input InputType { key: String! answer: Int = 42 } +input AnnotatedInput @onInputObjectType { + annotatedField: Type @onField +} + extend type Foo { seven(argument: [String]): Type } +extend type Foo @onType {} + type NoFields {} directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/src/language/ast.js b/src/language/ast.js index feda211f40..0f45446588 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -289,6 +289,7 @@ export type ScalarTypeDefinition = { kind: 'ScalarTypeDefinition'; loc?: ?Location; name: Name; + directives?: ?Array; } export type ObjectTypeDefinition = { @@ -296,6 +297,7 @@ export type ObjectTypeDefinition = { loc?: ?Location; name: Name; interfaces?: ?Array; + directives?: ?Array; fields: Array; } @@ -305,6 +307,7 @@ export type FieldDefinition = { name: Name; arguments: Array; type: Type; + directives?: ?Array; } export type InputValueDefinition = { @@ -313,12 +316,14 @@ export type InputValueDefinition = { name: Name; type: Type; defaultValue?: ?Value; + directives?: ?Array; } export type InterfaceTypeDefinition = { kind: 'InterfaceTypeDefinition'; loc?: ?Location; name: Name; + directives?: ?Array; fields: Array; } @@ -326,6 +331,7 @@ export type UnionTypeDefinition = { kind: 'UnionTypeDefinition'; loc?: ?Location; name: Name; + directives?: ?Array; types: Array; } @@ -333,6 +339,7 @@ export type EnumTypeDefinition = { kind: 'EnumTypeDefinition'; loc?: ?Location; name: Name; + directives?: ?Array; values: Array; } @@ -340,12 +347,14 @@ export type EnumValueDefinition = { kind: 'EnumValueDefinition'; loc?: ?Location; name: Name; + directives?: ?Array; } export type InputObjectTypeDefinition = { kind: 'InputObjectTypeDefinition'; loc?: ?Location; name: Name; + directives?: ?Array; fields: Array; } diff --git a/src/language/parser.js b/src/language/parser.js index 5d03376c4e..6195b9bfe1 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -735,27 +735,31 @@ function parseOperationTypeDefinition(parser: Parser): OperationTypeDefinition { } /** - * ScalarTypeDefinition : scalar Name + * ScalarTypeDefinition : scalar Name Directives? */ function parseScalarTypeDefinition(parser: Parser): ScalarTypeDefinition { const start = parser.token.start; expectKeyword(parser, 'scalar'); const name = parseName(parser); + const directives = parseDirectives(parser); return { kind: SCALAR_TYPE_DEFINITION, name, + directives, loc: loc(parser, start), }; } /** - * ObjectTypeDefinition : type Name ImplementsInterfaces? { FieldDefinition+ } + * ObjectTypeDefinition : + * - type Name ImplementsInterfaces? Directives? { FieldDefinition+ } */ function parseObjectTypeDefinition(parser: Parser): ObjectTypeDefinition { const start = parser.token.start; expectKeyword(parser, 'type'); const name = parseName(parser); const interfaces = parseImplementsInterfaces(parser); + const directives = parseDirectives(parser); const fields = any( parser, TokenKind.BRACE_L, @@ -766,6 +770,7 @@ function parseObjectTypeDefinition(parser: Parser): ObjectTypeDefinition { kind: OBJECT_TYPE_DEFINITION, name, interfaces, + directives, fields, loc: loc(parser, start), }; @@ -780,13 +785,13 @@ function parseImplementsInterfaces(parser: Parser): Array { advance(parser); do { types.push(parseNamedType(parser)); - } while (!peek(parser, TokenKind.BRACE_L)); + } while (peek(parser, TokenKind.NAME)); } return types; } /** - * FieldDefinition : Name ArgumentsDefinition? : Type + * FieldDefinition : Name ArgumentsDefinition? : Type Directives? */ function parseFieldDefinition(parser: Parser): FieldDefinition { const start = parser.token.start; @@ -794,11 +799,13 @@ function parseFieldDefinition(parser: Parser): FieldDefinition { const args = parseArgumentDefs(parser); expect(parser, TokenKind.COLON); const type = parseType(parser); + const directives = parseDirectives(parser); return { kind: FIELD_DEFINITION, name, arguments: args, type, + directives, loc: loc(parser, start), }; } @@ -814,7 +821,7 @@ function parseArgumentDefs(parser: Parser): Array { } /** - * InputValueDefinition : Name : Type DefaultValue? + * InputValueDefinition : Name : Type DefaultValue? Directives? */ function parseInputValueDef(parser: Parser): InputValueDefinition { const start = parser.token.start; @@ -825,22 +832,25 @@ function parseInputValueDef(parser: Parser): InputValueDefinition { if (skip(parser, TokenKind.EQUALS)) { defaultValue = parseConstValue(parser); } + const directives = parseDirectives(parser); return { kind: INPUT_VALUE_DEFINITION, name, type, defaultValue, + directives, loc: loc(parser, start), }; } /** - * InterfaceTypeDefinition : interface Name { FieldDefinition+ } + * InterfaceTypeDefinition : interface Name Directives? { FieldDefinition+ } */ function parseInterfaceTypeDefinition(parser: Parser): InterfaceTypeDefinition { const start = parser.token.start; expectKeyword(parser, 'interface'); const name = parseName(parser); + const directives = parseDirectives(parser); const fields = any( parser, TokenKind.BRACE_L, @@ -850,23 +860,26 @@ function parseInterfaceTypeDefinition(parser: Parser): InterfaceTypeDefinition { return { kind: INTERFACE_TYPE_DEFINITION, name, + directives, fields, loc: loc(parser, start), }; } /** - * UnionTypeDefinition : union Name = UnionMembers + * UnionTypeDefinition : union Name Directives? = UnionMembers */ function parseUnionTypeDefinition(parser: Parser): UnionTypeDefinition { const start = parser.token.start; expectKeyword(parser, 'union'); const name = parseName(parser); + const directives = parseDirectives(parser); expect(parser, TokenKind.EQUALS); const types = parseUnionMembers(parser); return { kind: UNION_TYPE_DEFINITION, name, + directives, types, loc: loc(parser, start), }; @@ -886,12 +899,13 @@ function parseUnionMembers(parser: Parser): Array { } /** - * EnumTypeDefinition : enum Name { EnumValueDefinition+ } + * EnumTypeDefinition : enum Name Directives? { EnumValueDefinition+ } */ function parseEnumTypeDefinition(parser: Parser): EnumTypeDefinition { const start = parser.token.start; expectKeyword(parser, 'enum'); const name = parseName(parser); + const directives = parseDirectives(parser); const values = many( parser, TokenKind.BRACE_L, @@ -901,28 +915,31 @@ function parseEnumTypeDefinition(parser: Parser): EnumTypeDefinition { return { kind: ENUM_TYPE_DEFINITION, name, + directives, values, loc: loc(parser, start), }; } /** - * EnumValueDefinition : EnumValue + * EnumValueDefinition : EnumValue Directives? * * EnumValue : Name */ function parseEnumValueDefinition(parser: Parser) : EnumValueDefinition { const start = parser.token.start; const name = parseName(parser); + const directives = parseDirectives(parser); return { kind: ENUM_VALUE_DEFINITION, name, + directives, loc: loc(parser, start), }; } /** - * InputObjectTypeDefinition : input Name { InputValueDefinition+ } + * InputObjectTypeDefinition : input Name Directives? { InputValueDefinition+ } */ function parseInputObjectTypeDefinition( parser: Parser @@ -930,6 +947,7 @@ function parseInputObjectTypeDefinition( const start = parser.token.start; expectKeyword(parser, 'input'); const name = parseName(parser); + const directives = parseDirectives(parser); const fields = any( parser, TokenKind.BRACE_L, @@ -939,6 +957,7 @@ function parseInputObjectTypeDefinition( return { kind: INPUT_OBJECT_TYPE_DEFINITION, name, + directives, fields, loc: loc(parser, start), }; diff --git a/src/language/printer.js b/src/language/printer.js index e368025d51..356d7c29bd 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -100,33 +100,65 @@ const printDocASTReducer = { OperationTypeDefinition: ({ operation, type }) => operation + ': ' + type, - ScalarTypeDefinition: ({ name }) => - `scalar ${name}`, + ScalarTypeDefinition: ({ name, directives }) => + join([ 'scalar', name, join(directives, ' ') ], ' '), - ObjectTypeDefinition: ({ name, interfaces, fields }) => - 'type ' + name + ' ' + - wrap('implements ', join(interfaces, ', '), ' ') + - block(fields), + ObjectTypeDefinition: ({ name, interfaces, directives, fields }) => + join([ + 'type', + name, + wrap('implements ', join(interfaces, ', ')), + join(directives, ' '), + block(fields) + ], ' '), - FieldDefinition: ({ name, arguments: args, type }) => - name + wrap('(', join(args, ', '), ')') + ': ' + type, + FieldDefinition: ({ name, arguments: args, type, directives }) => + name + + wrap('(', join(args, ', '), ')') + + ': ' + type + + wrap(' ', join(directives, ' ')), - InputValueDefinition: ({ name, type, defaultValue }) => - name + ': ' + type + wrap(' = ', defaultValue), + InputValueDefinition: ({ name, type, defaultValue, directives }) => + join([ + name + ': ' + type, + wrap('= ', defaultValue), + join(directives, ' ') + ], ' '), - InterfaceTypeDefinition: ({ name, fields }) => - `interface ${name} ${block(fields)}`, + InterfaceTypeDefinition: ({ name, directives, fields }) => + join([ + 'interface', + name, + join(directives, ' '), + block(fields) + ], ' '), - UnionTypeDefinition: ({ name, types }) => - `union ${name} = ${join(types, ' | ')}`, + UnionTypeDefinition: ({ name, directives, types }) => + join([ + 'union', + name, + join(directives, ' '), + '= ' + join(types, ' | ') + ], ' '), - EnumTypeDefinition: ({ name, values }) => - `enum ${name} ${block(values)}`, + EnumTypeDefinition: ({ name, directives, values }) => + join([ + 'enum', + name, + join(directives, ' '), + block(values) + ], ' '), - EnumValueDefinition: ({ name }) => name, + EnumValueDefinition: ({ name, directives }) => + join([ name, join(directives, ' ') ], ' '), - InputObjectTypeDefinition: ({ name, fields }) => - `input ${name} ${block(fields)}`, + InputObjectTypeDefinition: ({ name, directives, fields }) => + join([ + 'input', + name, + join(directives, ' '), + block(fields) + ], ' '), TypeExtensionDefinition: ({ definition }) => `extend ${definition}`, diff --git a/src/language/visitor.js b/src/language/visitor.js index 34ae887b7e..87661ff26e 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -41,15 +41,15 @@ export const QueryDocumentKeys = { SchemaDefinition: [ 'operationTypes' ], OperationTypeDefinition: [ 'type' ], - ScalarTypeDefinition: [ 'name' ], - ObjectTypeDefinition: [ 'name', 'interfaces', 'fields' ], - FieldDefinition: [ 'name', 'arguments', 'type' ], - InputValueDefinition: [ 'name', 'type', 'defaultValue' ], - InterfaceTypeDefinition: [ 'name', 'fields' ], - UnionTypeDefinition: [ 'name', 'types' ], - EnumTypeDefinition: [ 'name', 'values' ], - EnumValueDefinition: [ 'name' ], - InputObjectTypeDefinition: [ 'name', 'fields' ], + ScalarTypeDefinition: [ 'name', 'directives' ], + ObjectTypeDefinition: [ 'name', 'interfaces', 'directives', 'fields' ], + FieldDefinition: [ 'name', 'arguments', 'type', 'directives' ], + InputValueDefinition: [ 'name', 'type', 'defaultValue', 'directives' ], + InterfaceTypeDefinition: [ 'name', 'directives', 'fields' ], + UnionTypeDefinition: [ 'name', 'directives', 'types' ], + EnumTypeDefinition: [ 'name', 'directives', 'values' ], + EnumValueDefinition: [ 'name', 'directives' ], + InputObjectTypeDefinition: [ 'name', 'directives', 'fields' ], TypeExtensionDefinition: [ 'definition' ], diff --git a/src/type/directives.js b/src/type/directives.js index e93adb9674..72a2ec7075 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -19,6 +19,7 @@ import { assertValidName } from '../utilities/assertValidName'; export const DirectiveLocation = { + // Operations QUERY: 'QUERY', MUTATION: 'MUTATION', SUBSCRIPTION: 'SUBSCRIPTION', @@ -26,6 +27,17 @@ export const DirectiveLocation = { FRAGMENT_DEFINITION: 'FRAGMENT_DEFINITION', FRAGMENT_SPREAD: 'FRAGMENT_SPREAD', INLINE_FRAGMENT: 'INLINE_FRAGMENT', + // Schema Definitions + SCALAR: 'SCALAR', + OBJECT: 'OBJECT', + FIELD_DEFINITION: 'FIELD_DEFINITION', + ARGUMENT_DEFINITION: 'ARGUMENT_DEFINITION', + INTERFACE: 'INTERFACE', + UNION: 'UNION', + ENUM: 'ENUM', + ENUM_VALUE: 'ENUM_VALUE', + INPUT_OBJECT: 'INPUT_OBJECT', + INPUT_FIELD_DEFINITION: 'INPUT_FIELD_DEFINITION', }; export type DirectiveLocationEnum = $Keys; // eslint-disable-line diff --git a/src/type/introspection.js b/src/type/introspection.js index d760dac70f..383d9ac9db 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -149,6 +149,46 @@ export const __DirectiveLocation = new GraphQLEnumType({ value: DirectiveLocation.INLINE_FRAGMENT, description: 'Location adjacent to an inline fragment.' }, + SCALAR: { + value: DirectiveLocation.SCALAR, + description: 'Location adjacent to a scalar definition.' + }, + OBJECT: { + value: DirectiveLocation.OBJECT, + description: 'Location adjacent to an object type definition.' + }, + FIELD_DEFINITION: { + value: DirectiveLocation.FIELD_DEFINITION, + description: 'Location adjacent to a field definition.' + }, + ARGUMENT_DEFINITION: { + value: DirectiveLocation.ARGUMENT_DEFINITION, + description: 'Location adjacent to an argument definition.' + }, + INTERFACE: { + value: DirectiveLocation.INTERFACE, + description: 'Location adjacent to an interface definition.' + }, + UNION: { + value: DirectiveLocation.UNION, + description: 'Location adjacent to a union definition.' + }, + ENUM: { + value: DirectiveLocation.ENUM, + description: 'Location adjacent to an enum definition.' + }, + ENUM_VALUE: { + value: DirectiveLocation.ENUM_VALUE, + description: 'Location adjacent to an enum value definition.' + }, + INPUT_OBJECT: { + value: DirectiveLocation.INPUT_OBJECT, + description: 'Location adjacent to an input object type definition.' + }, + INPUT_FIELD_DEFINITION: { + value: DirectiveLocation.INPUT_FIELD_DEFINITION, + description: 'Location adjacent to an input object field definition.' + }, } }); diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index f9cb17c849..0e055edcef 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -620,6 +620,16 @@ enum __DirectiveLocation { FRAGMENT_DEFINITION FRAGMENT_SPREAD INLINE_FRAGMENT + SCALAR + OBJECT + FIELD_DEFINITION + ARGUMENT_DEFINITION + INTERFACE + UNION + ENUM + ENUM_VALUE + INPUT_OBJECT + INPUT_FIELD_DEFINITION } type __EnumValue { diff --git a/src/validation/__tests__/KnownDirectives-test.js b/src/validation/__tests__/KnownDirectives-test.js index e71f5f924b..91e9f063ce 100644 --- a/src/validation/__tests__/KnownDirectives-test.js +++ b/src/validation/__tests__/KnownDirectives-test.js @@ -7,6 +7,9 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +// 80+ char lines are useful in tests, so ignore in this file. +/* eslint-disable max-len */ + import { describe, it } from 'mocha'; import { expectPassesRule, expectFailsRule } from './harness'; import { @@ -92,26 +95,100 @@ describe('Validate: Known directives', () => { it('with well placed directives', () => { expectPassesRule(KnownDirectives, ` - query Foo { + query Foo @onQuery { name @include(if: true) ...Frag @include(if: true) skippedField @skip(if: true) ...SkippedFrag @skip(if: true) } + + mutation Bar @onMutation { + someField + } `); }); it('with misplaced directives', () => { expectFailsRule(KnownDirectives, ` query Foo @include(if: true) { - name @operationOnly - ...Frag @operationOnly + name @onQuery + ...Frag @onQuery + } + + mutation Bar @onQuery { + someField } `, [ misplacedDirective('include', 'QUERY', 2, 17), - misplacedDirective('operationOnly', 'FIELD', 3, 14), - misplacedDirective('operationOnly', 'FRAGMENT_SPREAD', 4, 17), + misplacedDirective('onQuery', 'FIELD', 3, 14), + misplacedDirective('onQuery', 'FRAGMENT_SPREAD', 4, 17), + misplacedDirective('onQuery', 'MUTATION', 7, 20), ]); }); + describe('within schema language', () => { + + it('with well placed directives', () => { + expectPassesRule(KnownDirectives, ` + type MyObj implements MyInterface @onObject { + myField(myArg: Int @onArgumentDefinition): String @onFieldDefinition + } + + scalar MyScalar @onScalar + + interface MyInterface @onInterface { + myField(myArg: Int @onArgumentDefinition): String @onFieldDefinition + } + + union MyUnion @onUnion = MyObj | Other + + enum MyEnum @onEnum { + MY_VALUE @onEnumValue + } + + input MyInput @onInputObject { + myField: Int @onInputFieldDefinition + } + `); + }); + + it('with misplaced directives', () => { + expectFailsRule(KnownDirectives, ` + type MyObj implements MyInterface @onInterface { + myField(myArg: Int @onInputFieldDefinition): String @onInputFieldDefinition + } + + scalar MyScalar @onEnum + + interface MyInterface @onObject { + myField(myArg: Int @onInputFieldDefinition): String @onInputFieldDefinition + } + + union MyUnion @onEnumValue = MyObj | Other + + enum MyEnum @onScalar { + MY_VALUE @onUnion + } + + input MyInput @onEnum { + myField: Int @onArgumentDefinition + } + `, [ + misplacedDirective('onInterface', 'OBJECT', 2, 43), + misplacedDirective('onInputFieldDefinition', 'ARGUMENT_DEFINITION', 3, 30), + misplacedDirective('onInputFieldDefinition', 'FIELD_DEFINITION', 3, 63), + misplacedDirective('onEnum', 'SCALAR', 6, 25), + misplacedDirective('onObject', 'INTERFACE', 8, 31), + misplacedDirective('onInputFieldDefinition', 'ARGUMENT_DEFINITION', 9, 30), + misplacedDirective('onInputFieldDefinition', 'FIELD_DEFINITION', 9, 63), + misplacedDirective('onEnumValue', 'UNION', 12, 23), + misplacedDirective('onScalar', 'ENUM', 14, 21), + misplacedDirective('onUnion', 'ENUM_VALUE', 15, 20), + misplacedDirective('onEnum', 'INPUT_OBJECT', 18, 23), + misplacedDirective('onArgumentDefinition', 'INPUT_FIELD_DEFINITION', 19, 24), + ]); + }); + + }); + }); diff --git a/src/validation/__tests__/harness.js b/src/validation/__tests__/harness.js index b9491e9b70..3c30bcd603 100644 --- a/src/validation/__tests__/harness.js +++ b/src/validation/__tests__/harness.js @@ -306,12 +306,76 @@ export const testSchema = new GraphQLSchema({ query: QueryRoot, types: [ Cat, Dog, Human, Alien ], directives: [ + GraphQLIncludeDirective, + GraphQLSkipDirective, new GraphQLDirective({ - name: 'operationOnly', + name: 'onQuery', locations: [ 'QUERY' ], }), - GraphQLIncludeDirective, - GraphQLSkipDirective, + new GraphQLDirective({ + name: 'onMutation', + locations: [ 'MUTATION' ], + }), + new GraphQLDirective({ + name: 'onSubscription', + locations: [ 'SUBSCRIPTION' ], + }), + new GraphQLDirective({ + name: 'onField', + locations: [ 'FIELD' ], + }), + new GraphQLDirective({ + name: 'onFragmentDefinition', + locations: [ 'FRAGMENT_DEFINITION' ], + }), + new GraphQLDirective({ + name: 'onFragmentSpread', + locations: [ 'FRAGMENT_SPREAD' ], + }), + new GraphQLDirective({ + name: 'onInlineFragment', + locations: [ 'INLINE_FRAGMENT' ], + }), + new GraphQLDirective({ + name: 'onScalar', + locations: [ 'SCALAR' ], + }), + new GraphQLDirective({ + name: 'onObject', + locations: [ 'OBJECT' ], + }), + new GraphQLDirective({ + name: 'onFieldDefinition', + locations: [ 'FIELD_DEFINITION' ], + }), + new GraphQLDirective({ + name: 'onArgumentDefinition', + locations: [ 'ARGUMENT_DEFINITION' ], + }), + new GraphQLDirective({ + name: 'onInterface', + locations: [ 'INTERFACE' ], + }), + new GraphQLDirective({ + name: 'onUnion', + locations: [ 'UNION' ], + }), + new GraphQLDirective({ + name: 'onEnum', + locations: [ 'ENUM' ], + }), + new GraphQLDirective({ + name: 'onEnumValue', + locations: [ 'ENUM_VALUE' ], + }), + new GraphQLDirective({ + name: 'onInputObject', + locations: [ 'INPUT_OBJECT' ], + }), + new GraphQLDirective({ + name: 'onInputFieldDefinition', + locations: [ 'INPUT_FIELD_DEFINITION' ], + }), ] }); diff --git a/src/validation/rules/KnownDirectives.js b/src/validation/rules/KnownDirectives.js index 5ed6383868..fdef4a1db4 100644 --- a/src/validation/rules/KnownDirectives.js +++ b/src/validation/rules/KnownDirectives.js @@ -12,11 +12,20 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; import find from '../../jsutils/find'; import { - OPERATION_DEFINITION, FIELD, + FRAGMENT_DEFINITION, FRAGMENT_SPREAD, INLINE_FRAGMENT, - FRAGMENT_DEFINITION + OPERATION_DEFINITION, + ENUM_TYPE_DEFINITION, + ENUM_VALUE_DEFINITION, + FIELD_DEFINITION, + INPUT_OBJECT_TYPE_DEFINITION, + INPUT_VALUE_DEFINITION, + INTERFACE_TYPE_DEFINITION, + OBJECT_TYPE_DEFINITION, + SCALAR_TYPE_DEFINITION, + UNION_TYPE_DEFINITION, } from '../../language/kinds'; import { DirectiveLocation } from '../../type/directives'; @@ -52,8 +61,7 @@ export function KnownDirectives(context: ValidationContext): any { )); return; } - const appliedTo = ancestors[ancestors.length - 1]; - const candidateLocation = getLocationForAppliedNode(appliedTo); + const candidateLocation = getDirectiveLocationForASTPath(ancestors); if (!candidateLocation) { context.reportError(new GraphQLError( misplacedDirectiveMessage(node.name.value, node.type), @@ -69,7 +77,8 @@ export function KnownDirectives(context: ValidationContext): any { }; } -function getLocationForAppliedNode(appliedTo) { +function getDirectiveLocationForASTPath(ancestors) { + const appliedTo = ancestors[ancestors.length - 1]; switch (appliedTo.kind) { case OPERATION_DEFINITION: switch (appliedTo.operation) { @@ -82,5 +91,18 @@ function getLocationForAppliedNode(appliedTo) { case FRAGMENT_SPREAD: return DirectiveLocation.FRAGMENT_SPREAD; case INLINE_FRAGMENT: return DirectiveLocation.INLINE_FRAGMENT; case FRAGMENT_DEFINITION: return DirectiveLocation.FRAGMENT_DEFINITION; + case SCALAR_TYPE_DEFINITION: return DirectiveLocation.SCALAR; + case OBJECT_TYPE_DEFINITION: return DirectiveLocation.OBJECT; + case FIELD_DEFINITION: return DirectiveLocation.FIELD_DEFINITION; + case INTERFACE_TYPE_DEFINITION: return DirectiveLocation.INTERFACE; + case UNION_TYPE_DEFINITION: return DirectiveLocation.UNION; + case ENUM_TYPE_DEFINITION: return DirectiveLocation.ENUM; + case ENUM_VALUE_DEFINITION: return DirectiveLocation.ENUM_VALUE; + case INPUT_OBJECT_TYPE_DEFINITION: return DirectiveLocation.INPUT_OBJECT; + case INPUT_VALUE_DEFINITION: + const parentNode = ancestors[ancestors.length - 3]; + return parentNode.kind === INPUT_OBJECT_TYPE_DEFINITION ? + DirectiveLocation.INPUT_FIELD_DEFINITION : + DirectiveLocation.ARGUMENT_DEFINITION; } }