From 062ccc4ba3fbd72efbf1a9cbd1de1a0f42c95a26 Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Thu, 5 Dec 2019 18:27:31 -0800 Subject: [PATCH] Add `@specified` directive This in an implementation for a spec proposal: * Spec proposal: [[RFC] Custom Scalar Specification URIs](https://github.com/graphql/graphql-spec/pull/649) * Original issue: [[RFC] Custom Scalar Specification URIs](https://github.com/graphql/graphql-spec/issues/635) --- src/type/__tests__/introspection-test.js | 41 +++++++++++++++++++ src/type/definition.d.ts | 1 + src/type/directives.d.ts | 6 +++ src/type/directives.js | 16 ++++++++ src/type/introspection.js | 19 ++++++++- .../__tests__/buildASTSchema-test.js | 19 ++++++--- .../__tests__/findBreakingChanges-test.js | 7 +++- src/utilities/__tests__/schemaPrinter-test.js | 18 +++++++- src/utilities/buildASTSchema.js | 5 +++ src/utilities/getIntrospectionQuery.js | 1 + 10 files changed, 123 insertions(+), 10 deletions(-) diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 43d597671c8..c3a04941d6a 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -61,6 +61,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedBy: null, }, { kind: 'SCALAR', @@ -70,6 +71,7 @@ describe('Introspection', () => { interfaces: null, enumValues: null, possibleTypes: null, + specifiedBy: null, }, { kind: 'OBJECT', @@ -163,6 +165,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedBy: null, }, { kind: 'OBJECT', @@ -205,6 +208,17 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + args: [], + deprecationReason: null, + isDeprecated: false, + name: 'specifiedBy', + type: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, { name: 'fields', args: [ @@ -336,6 +350,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedBy: null, }, { kind: 'ENUM', @@ -386,6 +401,7 @@ describe('Introspection', () => { }, ], possibleTypes: null, + specifiedBy: null, }, { kind: 'SCALAR', @@ -395,6 +411,7 @@ describe('Introspection', () => { interfaces: null, enumValues: null, possibleTypes: null, + specifiedBy: null, }, { kind: 'OBJECT', @@ -495,6 +512,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedBy: null, }, { kind: 'OBJECT', @@ -557,6 +575,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedBy: null, }, { kind: 'OBJECT', @@ -619,6 +638,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedBy: null, }, { kind: 'OBJECT', @@ -701,6 +721,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedBy: null, }, { kind: 'ENUM', @@ -806,6 +827,7 @@ describe('Introspection', () => { }, ], possibleTypes: null, + specifiedBy: null, }, ], directives: [ @@ -847,6 +869,25 @@ describe('Introspection', () => { }, ], }, + { + name: 'specified', + locations: ['SCALAR'], + args: [ + { + defaultValue: null, + name: 'by', + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + }, + ], + }, { name: 'deprecated', locations: ['FIELD_DEFINITION', 'ENUM_VALUE'], diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index 01939fec1d7..98789978e68 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -409,6 +409,7 @@ export interface GraphQLObjectTypeConfig< > { name: string; description?: Maybe; + specifiedBy?: string; interfaces?: Thunk>; fields: Thunk>; isTypeOf?: Maybe>; diff --git a/src/type/directives.d.ts b/src/type/directives.d.ts index c6b86898100..67595e6e565 100644 --- a/src/type/directives.d.ts +++ b/src/type/directives.d.ts @@ -54,6 +54,12 @@ export const GraphQLIncludeDirective: GraphQLDirective; */ export const GraphQLSkipDirective: GraphQLDirective; +/** + * Used to provide an RFC3986-compliant URI for specifying the behaviour of + * custom scalar definitions. + */ +export const GraphQLSpecifiedDirective: GraphQLDirective; + /** * Constant string used for default reason for a deprecation. */ diff --git a/src/type/directives.js b/src/type/directives.js index 6b42a5e8709..a0470320f2b 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -167,6 +167,21 @@ export const GraphQLSkipDirective = new GraphQLDirective({ }, }); +/** + * Used to provide URL for specifying the behaviour of custom scalar definitions. + */ +export const GraphQLSpecifiedDirective = new GraphQLDirective({ + name: 'specified', + description: 'Exposes a URL that specifies the behaviour of this scalar.', + locations: [DirectiveLocation.SCALAR], + args: { + by: { + type: GraphQLNonNull(GraphQLString), + description: 'The URL that specifies the behaviour of this scalar.', + }, + }, +}); + /** * Constant string used for default reason for a deprecation. */ @@ -195,6 +210,7 @@ export const GraphQLDeprecatedDirective = new GraphQLDirective({ export const specifiedDirectives = Object.freeze([ GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLSpecifiedDirective, GraphQLDeprecatedDirective, ]); diff --git a/src/type/introspection.js b/src/type/introspection.js index a3b099462ee..22a78abaec7 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -8,9 +8,10 @@ import invariant from '../jsutils/invariant'; import { print } from '../language/printer'; import { DirectiveLocation } from '../language/directiveLocation'; import { astFromValue } from '../utilities/astFromValue'; +import { getDirectiveValues } from '../execution/values'; import { type GraphQLSchema } from './schema'; -import { type GraphQLDirective } from './directives'; +import { type GraphQLDirective, GraphQLSpecifiedDirective } from './directives'; import { GraphQLString, GraphQLBoolean } from './scalars'; import { type GraphQLType, @@ -184,7 +185,7 @@ export const __DirectiveLocation = new GraphQLEnumType({ export const __Type = new GraphQLObjectType({ name: '__Type', description: - 'The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.', + 'The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional specifiedBy URL, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.', fields: () => ({ kind: { @@ -221,6 +222,20 @@ export const __Type = new GraphQLObjectType({ resolve: obj => obj.description !== undefined ? obj.description : undefined, }, + specifiedBy: { + type: GraphQLString, + resolve: type => { + if (!isScalarType(type) || !type.astNode) { + return null; + } + + const specified = getDirectiveValues( + GraphQLSpecifiedDirective, + type.astNode, + ); + return specified && specified.by; + }, + }, fields: { type: GraphQLList(GraphQLNonNull(__Field)), args: { diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 1d82a83f29c..1b64534b92f 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -15,6 +15,7 @@ import { assertDirective, GraphQLSkipDirective, GraphQLIncludeDirective, + GraphQLSpecifiedDirective, GraphQLDeprecatedDirective, } from '../../type/directives'; import { @@ -182,12 +183,15 @@ describe('Schema Builder', () => { expect(cycleSDL(sdl, { commentDescriptions: true })).to.equal(sdl); }); - it('Maintains @skip & @include', () => { + it('Maintains @include, @skip & @specified', () => { const schema = buildSchema('type Query'); - expect(schema.getDirectives()).to.have.lengthOf(3); + expect(schema.getDirectives()).to.have.lengthOf(4); expect(schema.getDirective('skip')).to.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.equal(GraphQLIncludeDirective); + expect(schema.getDirective('specified')).to.equal( + GraphQLSpecifiedDirective, + ); expect(schema.getDirective('deprecated')).to.equal( GraphQLDeprecatedDirective, ); @@ -197,27 +201,32 @@ describe('Schema Builder', () => { const schema = buildSchema(` directive @skip on FIELD directive @include on FIELD + directive @specified on FIELD_DEFINITION directive @deprecated on FIELD_DEFINITION `); - expect(schema.getDirectives()).to.have.lengthOf(3); + expect(schema.getDirectives()).to.have.lengthOf(4); expect(schema.getDirective('skip')).to.not.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.not.equal( GraphQLIncludeDirective, ); + expect(schema.getDirective('specified')).to.not.equal( + GraphQLSpecifiedDirective, + ); expect(schema.getDirective('deprecated')).to.not.equal( GraphQLDeprecatedDirective, ); }); - it('Adding directives maintains @skip & @include', () => { + it('Adding directives maintains @include, @skip & @specified', () => { const schema = buildSchema(` directive @foo(arg: Int) on FIELD `); - expect(schema.getDirectives()).to.have.lengthOf(4); + expect(schema.getDirectives()).to.have.lengthOf(5); expect(schema.getDirective('skip')).to.not.equal(undefined); expect(schema.getDirective('include')).to.not.equal(undefined); + expect(schema.getDirective('specified')).to.not.equal(undefined); expect(schema.getDirective('deprecated')).to.not.equal(undefined); }); diff --git a/src/utilities/__tests__/findBreakingChanges-test.js b/src/utilities/__tests__/findBreakingChanges-test.js index b3fd7dae6e9..9dbcbf87f48 100644 --- a/src/utilities/__tests__/findBreakingChanges-test.js +++ b/src/utilities/__tests__/findBreakingChanges-test.js @@ -7,6 +7,7 @@ import { GraphQLSchema } from '../../type/schema'; import { GraphQLSkipDirective, GraphQLIncludeDirective, + GraphQLSpecifiedDirective, GraphQLDeprecatedDirective, } from '../../type/directives'; @@ -790,7 +791,11 @@ describe('findBreakingChanges', () => { const oldSchema = new GraphQLSchema({}); const newSchema = new GraphQLSchema({ - directives: [GraphQLSkipDirective, GraphQLIncludeDirective], + directives: [ + GraphQLSkipDirective, + GraphQLIncludeDirective, + GraphQLSpecifiedDirective, + ], }); expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([ diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 7b245ac2c21..02e1b1ef194 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -590,6 +590,12 @@ describe('Type System Printer', () => { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + """Exposes a URL that specifies the behaviour of this scalar.""" + directive @specified( + """The URL that specifies the behaviour of this scalar.""" + by: String! + ) on SCALAR + """Marks an element of a GraphQL schema as no longer supported.""" directive @deprecated( """ @@ -735,12 +741,13 @@ describe('Type System Printer', () => { """ The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the \`__TypeKind\` enum. - Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. + Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional specifiedBy URL, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. """ type __Type { kind: __TypeKind! name: String description: String + specifiedBy: String fields(includeDeprecated: Boolean = false): [__Field!] interfaces: [__Type!] possibleTypes: [__Type!] @@ -803,6 +810,12 @@ describe('Type System Printer', () => { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + # Exposes a URL that specifies the behaviour of this scalar. + directive @specified( + # The URL that specifies the behaviour of this scalar. + by: String! + ) on SCALAR + # Marks an element of a GraphQL schema as no longer supported. directive @deprecated( # Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/). @@ -927,11 +940,12 @@ describe('Type System Printer', () => { # The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the \`__TypeKind\` enum. # - # Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. + # Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional specifiedBy URL, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. type __Type { kind: __TypeKind! name: String description: String + specifiedBy: String fields(includeDeprecated: Boolean = false): [__Field!] interfaces: [__Type!] possibleTypes: [__Type!] diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 4fbb763ffa1..414c8d5c736 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -55,6 +55,7 @@ import { GraphQLSkipDirective, GraphQLIncludeDirective, GraphQLDeprecatedDirective, + GraphQLSpecifiedDirective, } from '../type/directives'; import { type GraphQLType, @@ -170,6 +171,10 @@ export function buildASTSchema( directives.push(GraphQLIncludeDirective); } + if (!directives.some(directive => directive.name === 'specified')) { + directives.push(GraphQLSpecifiedDirective); + } + if (!directives.some(directive => directive.name === 'deprecated')) { directives.push(GraphQLDeprecatedDirective); } diff --git a/src/utilities/getIntrospectionQuery.js b/src/utilities/getIntrospectionQuery.js index ceb22a8dcf9..c511cd358d2 100644 --- a/src/utilities/getIntrospectionQuery.js +++ b/src/utilities/getIntrospectionQuery.js @@ -34,6 +34,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { kind name ${descriptions ? 'description' : ''} + specifiedBy fields(includeDeprecated: true) { name ${descriptions ? 'description' : ''}