From 7ac3ad2c2facc84813f856d43e00e59bf6c5517c Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Thu, 5 Dec 2019 18:27:31 -0800 Subject: [PATCH 01/13] Add `@specifiedBy` 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) --- docs/APIReference-TypeSystem.md | 1 + src/type/__tests__/definition-test.js | 23 ++++++++++ src/type/__tests__/introspection-test.js | 42 +++++++++++++++++++ src/type/definition.d.ts | 3 ++ src/type/definition.js | 13 ++++++ src/type/directives.d.ts | 5 +++ src/type/directives.js | 16 +++++++ src/type/introspection.js | 7 +++- .../__tests__/buildASTSchema-test.js | 38 ++++++++++++++--- .../__tests__/buildClientSchema-test.js | 12 ++++++ src/utilities/__tests__/extendSchema-test.js | 27 ++++++++++++ .../__tests__/findBreakingChanges-test.js | 7 +++- src/utilities/__tests__/schemaPrinter-test.js | 31 +++++++++++++- src/utilities/buildASTSchema.js | 5 +++ src/utilities/buildClientSchema.js | 1 + src/utilities/extendSchema.js | 20 +++++++++ src/utilities/getIntrospectionQuery.d.ts | 1 + src/utilities/getIntrospectionQuery.js | 2 + src/utilities/printSchema.js | 19 ++++++++- 19 files changed, 263 insertions(+), 10 deletions(-) diff --git a/docs/APIReference-TypeSystem.md b/docs/APIReference-TypeSystem.md index b777db1ad1..83e5512173 100644 --- a/docs/APIReference-TypeSystem.md +++ b/docs/APIReference-TypeSystem.md @@ -209,6 +209,7 @@ type GraphQLScalarTypeConfig = { serialize: (value: mixed) => ?InternalType; parseValue?: (value: mixed) => ?InternalType; parseLiteral?: (valueAST: Value) => ?InternalType; + specifiedByUrl?: string; } ``` diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index 25803830a2..bc26f58cb2 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -49,6 +49,16 @@ describe('Type System: Scalars', () => { expect(() => new GraphQLScalarType({ name: 'SomeScalar' })).to.not.throw(); }); + it('accepts a Scalar type defining specifiedByUrl', () => { + expect( + () => + new GraphQLScalarType({ + name: 'SomeScalar', + specifiedByUrl: 'https://example.com/foo_spec', + }), + ).not.to.throw(); + }); + it('accepts a Scalar type defining parseValue and parseLiteral', () => { expect( () => @@ -128,6 +138,19 @@ describe('Type System: Scalars', () => { 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.', ); }); + + it('rejects a Scalar type defining specifiedByUrl with an incorrect type', () => { + expect( + () => + new GraphQLScalarType({ + name: 'SomeScalar', + // $DisableFlowOnNegativeTest + specifiedByUrl: {}, + }), + ).to.throw( + 'SomeScalar must provide "specifiedByUrl" as a string, but got: {}.', + ); + }); }); describe('Type System: Objects', () => { diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 5c60e8a071..70a5a74630 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -63,6 +63,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedByUrl: null, }, { kind: 'SCALAR', @@ -72,6 +73,7 @@ describe('Introspection', () => { interfaces: null, enumValues: null, possibleTypes: null, + specifiedByUrl: null, }, { kind: 'SCALAR', @@ -81,6 +83,7 @@ describe('Introspection', () => { interfaces: null, enumValues: null, possibleTypes: null, + specifiedByUrl: null, }, { kind: 'OBJECT', @@ -185,6 +188,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedByUrl: null, }, { kind: 'OBJECT', @@ -227,6 +231,17 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + name: 'specifiedByUrl', + args: [], + type: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + isDeprecated: false, + deprecationReason: null, + }, { name: 'fields', args: [ @@ -358,6 +373,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedByUrl: null, }, { kind: 'ENUM', @@ -408,6 +424,7 @@ describe('Introspection', () => { }, ], possibleTypes: null, + specifiedByUrl: null, }, { kind: 'OBJECT', @@ -508,6 +525,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedByUrl: null, }, { kind: 'OBJECT', @@ -570,6 +588,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedByUrl: null, }, { kind: 'OBJECT', @@ -632,6 +651,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedByUrl: null, }, { kind: 'OBJECT', @@ -729,6 +749,7 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, + specifiedByUrl: null, }, { kind: 'ENUM', @@ -834,6 +855,7 @@ describe('Introspection', () => { }, ], possibleTypes: null, + specifiedByUrl: null, }, ], directives: [ @@ -877,6 +899,26 @@ describe('Introspection', () => { }, ], }, + { + name: 'specifiedBy', + isRepeatable: false, + locations: ['SCALAR'], + args: [ + { + defaultValue: null, + name: 'url', + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + }, + ], + }, { name: 'deprecated', isRepeatable: false, diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index 530a01a7e7..1134b9a469 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -297,6 +297,7 @@ export class GraphQLScalarType { extensions: Maybe>>; astNode: Maybe; extensionASTNodes: Maybe>; + specifiedByUrl?: Maybe; constructor(config: Readonly>); @@ -306,6 +307,7 @@ export class GraphQLScalarType { parseLiteral: GraphQLScalarLiteralParser; extensions: Maybe>>; extensionASTNodes: ReadonlyArray; + specifiedByUrl: Maybe; }; toString(): string; @@ -336,6 +338,7 @@ export interface GraphQLScalarTypeConfig { extensions?: Maybe>>; astNode?: Maybe; extensionASTNodes?: Maybe>; + specifiedByUrl?: Maybe; } /** diff --git a/src/type/definition.js b/src/type/definition.js index 16cd2de08e..dd714d3735 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -574,6 +574,7 @@ export class GraphQLScalarType { extensions: ?ReadOnlyObjMap; astNode: ?ScalarTypeDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; + specifiedByUrl: ?string; constructor(config: $ReadOnly>): void { const parseValue = config.parseValue ?? identityFunc; @@ -586,6 +587,7 @@ export class GraphQLScalarType { this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); + this.specifiedByUrl = config.specifiedByUrl; devAssert(typeof config.name === 'string', 'Must provide name.'); devAssert( @@ -600,6 +602,14 @@ export class GraphQLScalarType { `${this.name} must provide both "parseValue" and "parseLiteral" functions.`, ); } + + if (config.specifiedByUrl != null) { + devAssert( + typeof config.specifiedByUrl === 'string', + `${this.name} must provide "specifiedByUrl" as a string, ` + + `but got: ${inspect(config.specifiedByUrl)}.`, + ); + } } toConfig(): {| @@ -609,6 +619,7 @@ export class GraphQLScalarType { parseLiteral: GraphQLScalarLiteralParser, extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, + specifiedByUrl: ?string, |} { return { name: this.name, @@ -619,6 +630,7 @@ export class GraphQLScalarType { extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes ?? [], + specifiedByUrl: this.specifiedByUrl, }; } @@ -659,6 +671,7 @@ export type GraphQLScalarTypeConfig = {| extensions?: ?ReadOnlyObjMapLike, astNode?: ?ScalarTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, + specifiedByUrl?: ?string, |}; /** diff --git a/src/type/directives.d.ts b/src/type/directives.d.ts index 8865cdcc6b..4c6801485d 100644 --- a/src/type/directives.d.ts +++ b/src/type/directives.d.ts @@ -59,6 +59,11 @@ export const GraphQLIncludeDirective: GraphQLDirective; */ export const GraphQLSkipDirective: GraphQLDirective; +/** + * Used to provide a URL for specifying the behavior of custom scalar definitions. + */ +export const GraphQLSpecifiedByDirective: GraphQLDirective; + /** * Constant string used for default reason for a deprecation. */ diff --git a/src/type/directives.js b/src/type/directives.js index d3ed6fdae9..882e74e5b4 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -170,6 +170,21 @@ export const GraphQLSkipDirective = new GraphQLDirective({ }, }); +/** + * Used to provide a URL for specifying the behaviour of custom scalar definitions. + */ +export const GraphQLSpecifiedByDirective = new GraphQLDirective({ + name: 'specifiedBy', + description: 'Exposes a URL that specifies the behaviour of this scalar.', + locations: [DirectiveLocation.SCALAR], + args: { + url: { + type: GraphQLNonNull(GraphQLString), + description: 'The URL that specifies the behaviour of this scalar.', + }, + }, +}); + /** * Constant string used for default reason for a deprecation. */ @@ -198,6 +213,7 @@ export const GraphQLDeprecatedDirective = new GraphQLDirective({ export const specifiedDirectives = Object.freeze([ GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLSpecifiedByDirective, GraphQLDeprecatedDirective, ]); diff --git a/src/type/introspection.js b/src/type/introspection.js index 62f53325c5..0b2eb14527 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -195,7 +195,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 specifiedByUrl, 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: { @@ -239,6 +239,11 @@ export const __Type = new GraphQLObjectType({ resolve: (type) => type.description !== undefined ? type.description : undefined, }, + specifiedByUrl: { + type: GraphQLString, + resolve: obj => + obj.specifiedByUrl !== undefined ? obj.specifiedByUrl : undefined, + }, fields: { type: GraphQLList(GraphQLNonNull(__Field)), args: { diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 2238294e14..a099157cec 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -16,6 +16,7 @@ import { assertDirective, GraphQLSkipDirective, GraphQLIncludeDirective, + GraphQLSpecifiedByDirective, GraphQLDeprecatedDirective, } from '../../type/directives'; import { @@ -215,12 +216,15 @@ describe('Schema Builder', () => { expect(cycleSDL(sdl, { commentDescriptions: true })).to.equal(sdl); }); - it('Maintains @skip & @include', () => { + it('Maintains @include, @skip & @specifiedBy', () => { 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('specifiedBy')).to.equal( + GraphQLSpecifiedByDirective, + ); expect(schema.getDirective('deprecated')).to.equal( GraphQLDeprecatedDirective, ); @@ -230,27 +234,32 @@ describe('Schema Builder', () => { const schema = buildSchema(` directive @skip on FIELD directive @include on FIELD + directive @specifiedBy 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('specifiedBy')).to.not.equal( + GraphQLSpecifiedByDirective, + ); expect(schema.getDirective('deprecated')).to.not.equal( GraphQLDeprecatedDirective, ); }); - it('Adding directives maintains @skip & @include', () => { + it('Adding directives maintains @include, @skip & @specifiedBy', () => { 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('specifiedBy')).to.not.equal(undefined); expect(schema.getDirective('deprecated')).to.not.equal(undefined); }); @@ -770,6 +779,25 @@ describe('Schema Builder', () => { }); }); + it('Supports @specifiedBy', () => { + const sdl = dedent` + scalar Foo @specifiedBy(url: "https://example.com/foo_spec") + + type Query { + foo: Foo @deprecated + } + `; + expect(cycleSDL(sdl)).to.equal(sdl); + + const schema = buildSchema(sdl); + + const foo = assertScalarType(schema.getType('Foo')); + + expect(foo).to.include({ + specifiedByUrl: 'https://example.com/foo_spec', + }); + }); + it('Correctly extend scalar type', () => { const scalarSDL = dedent` scalar SomeScalar diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index 3cc63ebfe9..2c539e7189 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -533,6 +533,18 @@ describe('Type System: build schema from introspection', () => { expect(cycleIntrospection(sdl)).to.equal(sdl); }); + it('builds a schema with specifiedBy url', () => { + const sdl = dedent` + scalar Foo @specifiedBy(url: "https://example.com/foo_spec") + + type Query { + foo: Foo + } + `; + + expect(cycleIntrospection(sdl)).to.equal(sdl); + }); + it('can use client schema for limited execution', () => { const schema = buildSchema(` scalar CustomScalar diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 5f52ec5bda..52b5dd6107 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -359,6 +359,33 @@ describe('extendSchema', () => { expect(printExtensionNodes(someScalar)).to.deep.equal(extensionSDL); }); + it('extends scalars by adding specifiedBy directive', () => { + const schema = buildSchema(` + type Query { + foo: Foo + } + + scalar Foo + + directive @foo on SCALAR + `); + const extensionSDL = dedent` + extend scalar Foo @foo + + extend scalar Foo @specifiedBy(url: "https://example.com/foo_spec") + `; + + const extendedSchema = extendSchema(schema, parse(extensionSDL)); + const foo = assertScalarType(extendedSchema.getType('Foo')); + + expect(foo.toConfig().specifiedByUrl).to.equal( + 'https://example.com/foo_spec', + ); + + expect(validateSchema(extendedSchema)).to.deep.equal([]); + expect(printExtensionNodes(foo)).to.deep.equal(extensionSDL); + }); + it('correctly assign AST nodes to new and extended types', () => { const schema = buildSchema(` type Query diff --git a/src/utilities/__tests__/findBreakingChanges-test.js b/src/utilities/__tests__/findBreakingChanges-test.js index 7246f8b413..1c5aad92a2 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, + GraphQLSpecifiedByDirective, GraphQLDeprecatedDirective, } from '../../type/directives'; @@ -799,7 +800,11 @@ describe('findBreakingChanges', () => { const oldSchema = new GraphQLSchema({}); const newSchema = new GraphQLSchema({ - directives: [GraphQLSkipDirective, GraphQLIncludeDirective], + directives: [ + GraphQLSkipDirective, + GraphQLIncludeDirective, + GraphQLSpecifiedByDirective, + ], }); expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([ diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index ebfe7f7ff9..c097c87050 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -505,6 +505,19 @@ describe('Type System Printer', () => { `); }); + it('Custom Scalar with specifiedByUrl', () => { + const FooType = new GraphQLScalarType({ + name: 'Foo', + specifiedByUrl: 'https://example.com/foo_spec', + }); + + const Schema = new GraphQLSchema({ types: [FooType] }); + const output = printForTest(Schema); + expect(output).to.equal(dedent` + scalar Foo @specifiedBy(url: "https://example.com/foo_spec") + `); + }); + it('Enum', () => { const RGBType = new GraphQLEnumType({ name: 'RGB', @@ -631,6 +644,12 @@ describe('Type System Printer', () => { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + """Exposes a URL that specifies the behaviour of this scalar.""" + directive @specifiedBy( + """The URL that specifies the behaviour of this scalar.""" + url: String! + ) on SCALAR + """Marks an element of a GraphQL schema as no longer supported.""" directive @deprecated( """ @@ -668,12 +687,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 specifiedByUrl, 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 + specifiedByUrl: String fields(includeDeprecated: Boolean = false): [__Field!] interfaces: [__Type!] possibleTypes: [__Type!] @@ -847,6 +867,12 @@ describe('Type System Printer', () => { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + # Exposes a URL that specifies the behaviour of this scalar. + directive @specifiedBy( + # The URL that specifies the behaviour of this scalar. + url: 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/). @@ -875,11 +901,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 specifiedByUrl, 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 + specifiedByUrl: String fields(includeDeprecated: Boolean = false): [__Field!] interfaces: [__Type!] possibleTypes: [__Type!] diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 7893071779..6073b9e864 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -17,6 +17,7 @@ import { GraphQLSkipDirective, GraphQLIncludeDirective, GraphQLDeprecatedDirective, + GraphQLSpecifiedByDirective, } from '../type/directives'; import { extendSchemaImpl } from './extendSchema'; @@ -102,6 +103,10 @@ export function buildASTSchema( directives.push(GraphQLIncludeDirective); } + if (!directives.some((directive) => directive.name === 'specifiedBy')) { + directives.push(GraphQLSpecifiedByDirective); + } + if (!directives.some((directive) => directive.name === 'deprecated')) { directives.push(GraphQLDeprecatedDirective); } diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index f1b52c80f0..0424567e6e 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -200,6 +200,7 @@ export function buildClientSchema( return new GraphQLScalarType({ name: scalarIntrospection.name, description: scalarIntrospection.description, + specifiedByUrl: scalarIntrospection.specifiedByUrl, }); } diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index f10195854b..829b45deb9 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -39,6 +39,8 @@ import { type EnumTypeExtensionNode, type EnumValueDefinitionNode, type DirectiveDefinitionNode, + type ScalarTypeDefinitionNode, + type ScalarTypeExtensionNode, } from '../language/ast'; import { assertValidSDLExtension } from '../validation/validate'; @@ -50,6 +52,7 @@ import { introspectionTypes, isIntrospectionType } from '../type/introspection'; import { GraphQLDirective, GraphQLDeprecatedDirective, + GraphQLSpecifiedByDirective, } from '../type/directives'; import { type GraphQLSchemaValidationOptions, @@ -324,9 +327,15 @@ export function extendSchemaImpl( const config = type.toConfig(); const extensions = typeExtensionsMap[config.name] ?? []; + const specifiedByUrl = [ + config.specifiedByUrl, + ...extensions.map(getSpecifiedByUrl), + ].filter(Boolean)[0]; + return new GraphQLScalarType({ ...config, extensionASTNodes: config.extensionASTNodes.concat(extensions), + specifiedByUrl, }); } @@ -660,6 +669,7 @@ export function extendSchemaImpl( description, astNode, extensionASTNodes, + specifiedByUrl: getSpecifiedByUrl(astNode), }); } case Kind.INPUT_OBJECT_TYPE_DEFINITION: { @@ -700,6 +710,16 @@ function getDeprecationReason( return (deprecated?.reason: any); } +/** + * Given a scalar node, returns the string value for the specifiedByUrl. + */ +function getSpecifiedByUrl( + node: ScalarTypeDefinitionNode | ScalarTypeExtensionNode, +): ?string { + const specifiedBy = getDirectiveValues(GraphQLSpecifiedByDirective, node); + return (specifiedBy?.url: any); +} + /** * Given an ast node, returns its string description. * @deprecated: provided to ease adoption and will be removed in v16. diff --git a/src/utilities/getIntrospectionQuery.d.ts b/src/utilities/getIntrospectionQuery.d.ts index ece6b71db4..da0fdcd186 100644 --- a/src/utilities/getIntrospectionQuery.d.ts +++ b/src/utilities/getIntrospectionQuery.d.ts @@ -53,6 +53,7 @@ export interface IntrospectionScalarType { readonly kind: 'SCALAR'; readonly name: string; readonly description?: Maybe; + readonly specifiedByUrl?: Maybe; } export interface IntrospectionObjectType { diff --git a/src/utilities/getIntrospectionQuery.js b/src/utilities/getIntrospectionQuery.js index 8f79aace3b..045faab732 100644 --- a/src/utilities/getIntrospectionQuery.js +++ b/src/utilities/getIntrospectionQuery.js @@ -58,6 +58,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { kind name ${descriptions} + specifiedByUrl fields(includeDeprecated: true) { name ${descriptions} @@ -166,6 +167,7 @@ export type IntrospectionScalarType = {| +kind: 'SCALAR', +name: string, +description?: ?string, + +specifiedByUrl: ?string, |}; export type IntrospectionObjectType = {| diff --git a/src/utilities/printSchema.js b/src/utilities/printSchema.js index fba396fb8d..e856abfea5 100644 --- a/src/utilities/printSchema.js +++ b/src/utilities/printSchema.js @@ -181,7 +181,11 @@ export function printType(type: GraphQLNamedType, options?: Options): string { } function printScalar(type: GraphQLScalarType, options): string { - return printDescription(options, type) + `scalar ${type.name}`; + return ( + printDescription(options, type) + + `scalar ${type.name}` + + printSpecifiedByUrl(type) + ); } function printImplementedInterfaces( @@ -321,6 +325,19 @@ function printDeprecated(fieldOrEnumVal) { return ' @deprecated'; } +function printSpecifiedByUrl(scalar: GraphQLScalarType) { + if (scalar.specifiedByUrl == null) { + return ''; + } + const url = scalar.specifiedByUrl; + const urlAST = astFromValue(url, GraphQLString); + invariant( + urlAST, + 'Unexpected null value returned from `astFromValue` for specifiedByUrl', + ); + return ' @specifiedBy(url: ' + print(urlAST) + ')'; +} + function printDescription( options, def, From 0dc4c53ed66b6ec1efc00c8e03b17ad9604b01f1 Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Wed, 4 Mar 2020 08:38:08 -0800 Subject: [PATCH 02/13] Backtick `specifiedByUrl` in description Co-Authored-By: christopher butcher --- src/type/introspection.js | 2 +- src/utilities/__tests__/schemaPrinter-test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/type/introspection.js b/src/type/introspection.js index 0b2eb14527..442fd799b6 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -195,7 +195,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, description and optional specifiedByUrl, 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 `specifiedByUrl`, 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: { diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index c097c87050..466c6ad174 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -687,7 +687,7 @@ 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, description and optional specifiedByUrl, 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 \`specifiedByUrl\`, 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! @@ -901,7 +901,7 @@ 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, description and optional specifiedByUrl, 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 \`specifiedByUrl\`, 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 From 363cf2bff011a384b49724e39846538cf92dcb3d Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Thu, 5 Mar 2020 09:27:55 -0800 Subject: [PATCH 03/13] Move `specifiedByUrl` between `description` and `serialize` --- docs/APIReference-TypeSystem.md | 2 +- src/type/definition.d.ts | 6 +++--- src/type/definition.js | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/APIReference-TypeSystem.md b/docs/APIReference-TypeSystem.md index 83e5512173..8efd840eb6 100644 --- a/docs/APIReference-TypeSystem.md +++ b/docs/APIReference-TypeSystem.md @@ -206,10 +206,10 @@ class GraphQLScalarType { type GraphQLScalarTypeConfig = { name: string; description?: ?string; + specifiedByUrl?: string; serialize: (value: mixed) => ?InternalType; parseValue?: (value: mixed) => ?InternalType; parseLiteral?: (valueAST: Value) => ?InternalType; - specifiedByUrl?: string; } ``` diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index 1134b9a469..75b9cde863 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -291,23 +291,23 @@ export type Thunk = (() => T) | T; export class GraphQLScalarType { name: string; description: Maybe; + specifiedByUrl?: Maybe; serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; parseLiteral: GraphQLScalarLiteralParser; extensions: Maybe>>; astNode: Maybe; extensionASTNodes: Maybe>; - specifiedByUrl?: Maybe; constructor(config: Readonly>); toConfig(): GraphQLScalarTypeConfig & { + specifiedByUrl: Maybe; serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; parseLiteral: GraphQLScalarLiteralParser; extensions: Maybe>>; extensionASTNodes: ReadonlyArray; - specifiedByUrl: Maybe; }; toString(): string; @@ -329,6 +329,7 @@ export type GraphQLScalarLiteralParser = ( export interface GraphQLScalarTypeConfig { name: string; description?: Maybe; + specifiedByUrl?: Maybe; // Serializes an internal value to include in a response. serialize: GraphQLScalarSerializer; // Parses an externally provided value to use as an input. @@ -338,7 +339,6 @@ export interface GraphQLScalarTypeConfig { extensions?: Maybe>>; astNode?: Maybe; extensionASTNodes?: Maybe>; - specifiedByUrl?: Maybe; } /** diff --git a/src/type/definition.js b/src/type/definition.js index dd714d3735..2ea5e32308 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -568,18 +568,19 @@ function undefineIfEmpty(arr: ?$ReadOnlyArray): ?$ReadOnlyArray { export class GraphQLScalarType { name: string; description: ?string; + specifiedByUrl: ?string; serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; parseLiteral: GraphQLScalarLiteralParser; extensions: ?ReadOnlyObjMap; astNode: ?ScalarTypeDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; - specifiedByUrl: ?string; constructor(config: $ReadOnly>): void { const parseValue = config.parseValue ?? identityFunc; this.name = config.name; this.description = config.description; + this.specifiedByUrl = config.specifiedByUrl; this.serialize = config.serialize ?? identityFunc; this.parseValue = parseValue; this.parseLiteral = @@ -587,7 +588,6 @@ export class GraphQLScalarType { this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); - this.specifiedByUrl = config.specifiedByUrl; devAssert(typeof config.name === 'string', 'Must provide name.'); devAssert( @@ -614,23 +614,23 @@ export class GraphQLScalarType { toConfig(): {| ...GraphQLScalarTypeConfig, + specifiedByUrl: ?string, serialize: GraphQLScalarSerializer, parseValue: GraphQLScalarValueParser, parseLiteral: GraphQLScalarLiteralParser, extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, - specifiedByUrl: ?string, |} { return { name: this.name, description: this.description, + specifiedByUrl: this.specifiedByUrl, serialize: this.serialize, parseValue: this.parseValue, parseLiteral: this.parseLiteral, extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes ?? [], - specifiedByUrl: this.specifiedByUrl, }; } @@ -662,6 +662,7 @@ export type GraphQLScalarLiteralParser = ( export type GraphQLScalarTypeConfig = {| name: string, description?: ?string, + specifiedByUrl?: ?string, // Serializes an internal value to include in a response. serialize?: GraphQLScalarSerializer, // Parses an externally provided value to use as an input. @@ -671,7 +672,6 @@ export type GraphQLScalarTypeConfig = {| extensions?: ?ReadOnlyObjMapLike, astNode?: ?ScalarTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, - specifiedByUrl?: ?string, |}; /** From d043c1166d13b3f5e9e41edd8effc79de2c3697f Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Thu, 5 Mar 2020 09:37:31 -0800 Subject: [PATCH 04/13] Refactor `specifiedByUrl` `devAssert` call --- src/type/definition.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/type/definition.js b/src/type/definition.js index 2ea5e32308..21773ed230 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -590,6 +590,14 @@ export class GraphQLScalarType { this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); devAssert(typeof config.name === 'string', 'Must provide name.'); + + devAssert( + config.specifiedByUrl == null || + typeof config.specifiedByUrl === 'string', + `${this.name} must provide "specifiedByUrl" as a string, ` + + `but got: ${inspect(config.specifiedByUrl)}.`, + ); + devAssert( config.serialize == null || typeof config.serialize === 'function', `${this.name} must provide "serialize" function. If this custom Scalar is also used as an input type, ensure "parseValue" and "parseLiteral" functions are also provided.`, @@ -602,14 +610,6 @@ export class GraphQLScalarType { `${this.name} must provide both "parseValue" and "parseLiteral" functions.`, ); } - - if (config.specifiedByUrl != null) { - devAssert( - typeof config.specifiedByUrl === 'string', - `${this.name} must provide "specifiedByUrl" as a string, ` + - `but got: ${inspect(config.specifiedByUrl)}.`, - ); - } } toConfig(): {| From f0e81e2d7f1bb6b00abb05e3df1a0695d1e109ce Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Thu, 5 Mar 2020 09:40:36 -0800 Subject: [PATCH 05/13] Minor refactor to "Supports @specifiedBy" test --- src/utilities/__tests__/buildASTSchema-test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index a099157cec..cef1db4ece 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -791,9 +791,7 @@ describe('Schema Builder', () => { const schema = buildSchema(sdl); - const foo = assertScalarType(schema.getType('Foo')); - - expect(foo).to.include({ + expect(schema.getType('Foo')).to.include({ specifiedByUrl: 'https://example.com/foo_spec', }); }); From 618e0f9b6881d9d171e2c69e0471830e6461e51d Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Thu, 5 Mar 2020 09:41:59 -0800 Subject: [PATCH 06/13] Remove unnecessary `.toConfig()` in test --- src/utilities/__tests__/extendSchema-test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 52b5dd6107..527c2a67e2 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -378,9 +378,7 @@ describe('extendSchema', () => { const extendedSchema = extendSchema(schema, parse(extensionSDL)); const foo = assertScalarType(extendedSchema.getType('Foo')); - expect(foo.toConfig().specifiedByUrl).to.equal( - 'https://example.com/foo_spec', - ); + expect(foo.specifiedByUrl).to.equal('https://example.com/foo_spec'); expect(validateSchema(extendedSchema)).to.deep.equal([]); expect(printExtensionNodes(foo)).to.deep.equal(extensionSDL); From 170c868c533453505434d0e0133d834d61e24037 Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Thu, 5 Mar 2020 09:57:07 -0800 Subject: [PATCH 07/13] Move `specifiedByUrl` between `description` and `serialize` --- src/utilities/extendSchema.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 829b45deb9..b37edef120 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -334,8 +334,8 @@ export function extendSchemaImpl( return new GraphQLScalarType({ ...config, - extensionASTNodes: config.extensionASTNodes.concat(extensions), specifiedByUrl, + extensionASTNodes: config.extensionASTNodes.concat(extensions), }); } @@ -667,9 +667,9 @@ export function extendSchemaImpl( return new GraphQLScalarType({ name, description, + specifiedByUrl: getSpecifiedByUrl(astNode), astNode, extensionASTNodes, - specifiedByUrl: getSpecifiedByUrl(astNode), }); } case Kind.INPUT_OBJECT_TYPE_DEFINITION: { From 1a48bcc68a5883ea4c4cc9cf8d5b6123128d54d2 Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Thu, 2 Apr 2020 07:33:55 -0700 Subject: [PATCH 08/13] Apply new prettier rules --- src/type/introspection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/type/introspection.js b/src/type/introspection.js index 442fd799b6..6ef4b0137c 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -241,7 +241,7 @@ export const __Type = new GraphQLObjectType({ }, specifiedByUrl: { type: GraphQLString, - resolve: obj => + resolve: (obj) => obj.specifiedByUrl !== undefined ? obj.specifiedByUrl : undefined, }, fields: { From d6e4f37a61f0f4597a008280822cec3a13e11794 Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Wed, 8 Apr 2020 09:35:24 -0700 Subject: [PATCH 09/13] Move `specifiedByUrl` after `name` in introspection-test --- src/type/__tests__/introspection-test.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 70a5a74630..e2ed1092a1 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -46,6 +46,7 @@ describe('Introspection', () => { { kind: 'OBJECT', name: 'QueryRoot', + specifiedByUrl: null, fields: [ { name: 'onlyField', @@ -63,31 +64,31 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, - specifiedByUrl: null, }, { kind: 'SCALAR', name: 'String', + specifiedByUrl: null, fields: null, inputFields: null, interfaces: null, enumValues: null, possibleTypes: null, - specifiedByUrl: null, }, { kind: 'SCALAR', name: 'Boolean', + specifiedByUrl: null, fields: null, inputFields: null, interfaces: null, enumValues: null, possibleTypes: null, - specifiedByUrl: null, }, { kind: 'OBJECT', name: '__Schema', + specifiedByUrl: null, fields: [ { name: 'description', @@ -188,11 +189,11 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, - specifiedByUrl: null, }, { kind: 'OBJECT', name: '__Type', + specifiedByUrl: null, fields: [ { name: 'kind', @@ -373,11 +374,11 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, - specifiedByUrl: null, }, { kind: 'ENUM', name: '__TypeKind', + specifiedByUrl: null, fields: null, inputFields: null, interfaces: null, @@ -424,11 +425,11 @@ describe('Introspection', () => { }, ], possibleTypes: null, - specifiedByUrl: null, }, { kind: 'OBJECT', name: '__Field', + specifiedByUrl: null, fields: [ { name: 'name', @@ -525,11 +526,11 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, - specifiedByUrl: null, }, { kind: 'OBJECT', name: '__InputValue', + specifiedByUrl: null, fields: [ { name: 'name', @@ -588,11 +589,11 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, - specifiedByUrl: null, }, { kind: 'OBJECT', name: '__EnumValue', + specifiedByUrl: null, fields: [ { name: 'name', @@ -651,7 +652,6 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, - specifiedByUrl: null, }, { kind: 'OBJECT', @@ -754,6 +754,7 @@ describe('Introspection', () => { { kind: 'ENUM', name: '__DirectiveLocation', + specifiedByUrl: null, fields: null, inputFields: null, interfaces: null, @@ -855,7 +856,6 @@ describe('Introspection', () => { }, ], possibleTypes: null, - specifiedByUrl: null, }, ], directives: [ From 22524b102198e2b9c5103ebdf989eb6d069d2943 Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Wed, 8 Apr 2020 09:39:39 -0700 Subject: [PATCH 10/13] GraphQLScalarType.specifiedByUrl is always set --- src/type/definition.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index 75b9cde863..437fd24791 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -291,7 +291,7 @@ export type Thunk = (() => T) | T; export class GraphQLScalarType { name: string; description: Maybe; - specifiedByUrl?: Maybe; + specifiedByUrl: Maybe; serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; parseLiteral: GraphQLScalarLiteralParser; From 64e81503c0184a0e26ad889aeb426e76976d2d4d Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Wed, 8 Apr 2020 09:43:25 -0700 Subject: [PATCH 11/13] Remove duplicated property in type. `specifiedByUrl` is already included when expanding `GraphQLScalarTypeConfig` --- src/type/definition.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/type/definition.js b/src/type/definition.js index 21773ed230..4cec43717c 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -614,7 +614,6 @@ export class GraphQLScalarType { toConfig(): {| ...GraphQLScalarTypeConfig, - specifiedByUrl: ?string, serialize: GraphQLScalarSerializer, parseValue: GraphQLScalarValueParser, parseLiteral: GraphQLScalarLiteralParser, From 17d4ad38831bd58d5142b9be524839496643258f Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Sat, 25 Apr 2020 10:10:09 -0700 Subject: [PATCH 12/13] Add option to not include `specifiedByUrl` in introspection query --- src/type/__tests__/introspection-test.js | 1 + src/utilities/__tests__/buildClientSchema-test.js | 5 ++++- .../__tests__/getIntrospectionQuery-test.js | 12 ++++++++++++ src/utilities/getIntrospectionQuery.d.ts | 4 ++++ src/utilities/getIntrospectionQuery.js | 10 +++++++++- 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index e2ed1092a1..26148f4ae3 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -31,6 +31,7 @@ describe('Introspection', () => { const source = getIntrospectionQuery({ descriptions: false, directiveIsRepeatable: true, + specifiedByUrl: true, }); const result = graphqlSync({ schema, source }); diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index 2c539e7189..7398413289 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -33,7 +33,10 @@ import { introspectionFromSchema } from '../introspectionFromSchema'; * returns that schema printed as SDL. */ function cycleIntrospection(sdlString: string): string { - const options = { directiveIsRepeatable: true }; + const options = { + directiveIsRepeatable: true, + specifiedByUrl: true, + }; const serverSchema = buildSchema(sdlString); const initialIntrospection = introspectionFromSchema(serverSchema, options); diff --git a/src/utilities/__tests__/getIntrospectionQuery-test.js b/src/utilities/__tests__/getIntrospectionQuery-test.js index e25a906e19..462d683acf 100644 --- a/src/utilities/__tests__/getIntrospectionQuery-test.js +++ b/src/utilities/__tests__/getIntrospectionQuery-test.js @@ -51,4 +51,16 @@ describe('getIntrospectionQuery', () => { getIntrospectionQuery({ descriptions: false, schemaDescription: true }), ).to.not.match(/\bdescription\b/); }); + + it('include "specifiedByUrl" field', () => { + expect(getIntrospectionQuery()).to.not.match(/\bspecifiedByUrl\b/); + + expect(getIntrospectionQuery({ specifiedByUrl: true })).to.match( + /\bspecifiedByUrl\b/, + ); + + expect(getIntrospectionQuery({ specifiedByUrl: false })).to.not.match( + /\bspecifiedByUrl\b/, + ); + }); }); diff --git a/src/utilities/getIntrospectionQuery.d.ts b/src/utilities/getIntrospectionQuery.d.ts index da0fdcd186..b1d5ecc174 100644 --- a/src/utilities/getIntrospectionQuery.d.ts +++ b/src/utilities/getIntrospectionQuery.d.ts @@ -6,6 +6,10 @@ export interface IntrospectionOptions { // Default: true descriptions: boolean; + // Whether to include `specifiedByUrl` in the introspection result. + // Default: false + specifiedByUrl?: boolean; + // Whether to include `isRepeatable` flag on directives. // Default: false directiveIsRepeatable?: boolean; diff --git a/src/utilities/getIntrospectionQuery.js b/src/utilities/getIntrospectionQuery.js index 045faab732..51e94cd9a1 100644 --- a/src/utilities/getIntrospectionQuery.js +++ b/src/utilities/getIntrospectionQuery.js @@ -7,6 +7,10 @@ export type IntrospectionOptions = {| // Default: true descriptions?: boolean, + // Whether to include `specifiedByUrl` in the introspection result. + // Default: false + specifiedByUrl?: boolean, + // Whether to include `isRepeatable` field on directives. // Default: false directiveIsRepeatable?: boolean, @@ -19,12 +23,16 @@ export type IntrospectionOptions = {| export function getIntrospectionQuery(options?: IntrospectionOptions): string { const optionsWithDefault = { descriptions: true, + specifiedByUrl: false, directiveIsRepeatable: false, schemaDescription: false, ...options, }; const descriptions = optionsWithDefault.descriptions ? 'description' : ''; + const specifiedByUrl = optionsWithDefault.specifiedByUrl + ? 'specifiedByUrl' + : ''; const directiveIsRepeatable = optionsWithDefault.directiveIsRepeatable ? 'isRepeatable' : ''; @@ -58,7 +66,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { kind name ${descriptions} - specifiedByUrl + ${specifiedByUrl} fields(includeDeprecated: true) { name ${descriptions} From fe0fd46982c41c449ddea278c2cbd518e2eb4b7e Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Thu, 7 May 2020 18:29:40 +0300 Subject: [PATCH 13/13] Review changes by @IvanGoncharov --- src/type/__tests__/introspection-test.js | 36 +++++++++---------- src/type/directives.js | 32 ++++++++--------- .../__tests__/buildASTSchema-test.js | 18 +++++----- .../__tests__/buildClientSchema-test.js | 2 +- src/utilities/__tests__/schemaPrinter-test.js | 24 ++++++------- src/utilities/buildASTSchema.js | 8 ++--- src/utilities/extendSchema.js | 8 ++--- 7 files changed, 64 insertions(+), 64 deletions(-) diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 26148f4ae3..1da64836c2 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -30,8 +30,8 @@ describe('Introspection', () => { }); const source = getIntrospectionQuery({ descriptions: false, - directiveIsRepeatable: true, specifiedByUrl: true, + directiveIsRepeatable: true, }); const result = graphqlSync({ schema, source }); @@ -657,6 +657,7 @@ describe('Introspection', () => { { kind: 'OBJECT', name: '__Directive', + specifiedByUrl: null, fields: [ { name: 'name', @@ -750,7 +751,6 @@ describe('Introspection', () => { interfaces: [], enumValues: null, possibleTypes: null, - specifiedByUrl: null, }, { kind: 'ENUM', @@ -900,6 +900,22 @@ describe('Introspection', () => { }, ], }, + { + name: 'deprecated', + isRepeatable: false, + locations: ['FIELD_DEFINITION', 'ENUM_VALUE'], + args: [ + { + defaultValue: '"No longer supported"', + name: 'reason', + type: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + ], + }, { name: 'specifiedBy', isRepeatable: false, @@ -920,22 +936,6 @@ describe('Introspection', () => { }, ], }, - { - name: 'deprecated', - isRepeatable: false, - locations: ['FIELD_DEFINITION', 'ENUM_VALUE'], - args: [ - { - defaultValue: '"No longer supported"', - name: 'reason', - type: { - kind: 'SCALAR', - name: 'String', - ofType: null, - }, - }, - ], - }, ], }, }, diff --git a/src/type/directives.js b/src/type/directives.js index 882e74e5b4..70b9a3ca6b 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -170,21 +170,6 @@ export const GraphQLSkipDirective = new GraphQLDirective({ }, }); -/** - * Used to provide a URL for specifying the behaviour of custom scalar definitions. - */ -export const GraphQLSpecifiedByDirective = new GraphQLDirective({ - name: 'specifiedBy', - description: 'Exposes a URL that specifies the behaviour of this scalar.', - locations: [DirectiveLocation.SCALAR], - args: { - url: { - type: GraphQLNonNull(GraphQLString), - description: 'The URL that specifies the behaviour of this scalar.', - }, - }, -}); - /** * Constant string used for default reason for a deprecation. */ @@ -207,14 +192,29 @@ export const GraphQLDeprecatedDirective = new GraphQLDirective({ }, }); +/** + * Used to provide a URL for specifying the behaviour of custom scalar definitions. + */ +export const GraphQLSpecifiedByDirective = new GraphQLDirective({ + name: 'specifiedBy', + description: 'Exposes a URL that specifies the behaviour of this scalar.', + locations: [DirectiveLocation.SCALAR], + args: { + url: { + type: GraphQLNonNull(GraphQLString), + description: 'The URL that specifies the behaviour of this scalar.', + }, + }, +}); + /** * The full list of specified directives. */ export const specifiedDirectives = Object.freeze([ GraphQLIncludeDirective, GraphQLSkipDirective, - GraphQLSpecifiedByDirective, GraphQLDeprecatedDirective, + GraphQLSpecifiedByDirective, ]); export function isSpecifiedDirective( diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index cef1db4ece..753ff73ead 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -16,8 +16,8 @@ import { assertDirective, GraphQLSkipDirective, GraphQLIncludeDirective, - GraphQLSpecifiedByDirective, GraphQLDeprecatedDirective, + GraphQLSpecifiedByDirective, } from '../../type/directives'; import { GraphQLID, @@ -222,20 +222,20 @@ describe('Schema Builder', () => { expect(schema.getDirectives()).to.have.lengthOf(4); expect(schema.getDirective('skip')).to.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.equal(GraphQLIncludeDirective); - expect(schema.getDirective('specifiedBy')).to.equal( - GraphQLSpecifiedByDirective, - ); expect(schema.getDirective('deprecated')).to.equal( GraphQLDeprecatedDirective, ); + expect(schema.getDirective('specifiedBy')).to.equal( + GraphQLSpecifiedByDirective, + ); }); it('Overriding directives excludes specified', () => { const schema = buildSchema(` directive @skip on FIELD directive @include on FIELD - directive @specifiedBy on FIELD_DEFINITION directive @deprecated on FIELD_DEFINITION + directive @specifiedBy on FIELD_DEFINITION `); expect(schema.getDirectives()).to.have.lengthOf(4); @@ -243,12 +243,12 @@ describe('Schema Builder', () => { expect(schema.getDirective('include')).to.not.equal( GraphQLIncludeDirective, ); - expect(schema.getDirective('specifiedBy')).to.not.equal( - GraphQLSpecifiedByDirective, - ); expect(schema.getDirective('deprecated')).to.not.equal( GraphQLDeprecatedDirective, ); + expect(schema.getDirective('specifiedBy')).to.not.equal( + GraphQLSpecifiedByDirective, + ); }); it('Adding directives maintains @include, @skip & @specifiedBy', () => { @@ -259,8 +259,8 @@ describe('Schema Builder', () => { 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('specifiedBy')).to.not.equal(undefined); expect(schema.getDirective('deprecated')).to.not.equal(undefined); + expect(schema.getDirective('specifiedBy')).to.not.equal(undefined); }); it('Type modifiers', () => { diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index 7398413289..85a7b1ca66 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -34,8 +34,8 @@ import { introspectionFromSchema } from '../introspectionFromSchema'; */ function cycleIntrospection(sdlString: string): string { const options = { - directiveIsRepeatable: true, specifiedByUrl: true, + directiveIsRepeatable: true, }; const serverSchema = buildSchema(sdlString); diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 466c6ad174..db1efddff5 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -644,12 +644,6 @@ describe('Type System Printer', () => { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - """Exposes a URL that specifies the behaviour of this scalar.""" - directive @specifiedBy( - """The URL that specifies the behaviour of this scalar.""" - url: String! - ) on SCALAR - """Marks an element of a GraphQL schema as no longer supported.""" directive @deprecated( """ @@ -658,6 +652,12 @@ describe('Type System Printer', () => { reason: String = "No longer supported" ) on FIELD_DEFINITION | ENUM_VALUE + """Exposes a URL that specifies the behaviour of this scalar.""" + directive @specifiedBy( + """The URL that specifies the behaviour of this scalar.""" + url: String! + ) on SCALAR + """ A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. """ @@ -867,18 +867,18 @@ describe('Type System Printer', () => { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - # Exposes a URL that specifies the behaviour of this scalar. - directive @specifiedBy( - # The URL that specifies the behaviour of this scalar. - url: 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/). reason: String = "No longer supported" ) on FIELD_DEFINITION | ENUM_VALUE + # Exposes a URL that specifies the behaviour of this scalar. + directive @specifiedBy( + # The URL that specifies the behaviour of this scalar. + url: String! + ) on SCALAR + # A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. type __Schema { description: String diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 6073b9e864..5299fc53a3 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -103,14 +103,14 @@ export function buildASTSchema( directives.push(GraphQLIncludeDirective); } - if (!directives.some((directive) => directive.name === 'specifiedBy')) { - directives.push(GraphQLSpecifiedByDirective); - } - if (!directives.some((directive) => directive.name === 'deprecated')) { directives.push(GraphQLDeprecatedDirective); } + if (!directives.some((directive) => directive.name === 'specifiedBy')) { + directives.push(GraphQLSpecifiedByDirective); + } + return new GraphQLSchema(config); } diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index b37edef120..8265973387 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -327,10 +327,10 @@ export function extendSchemaImpl( const config = type.toConfig(); const extensions = typeExtensionsMap[config.name] ?? []; - const specifiedByUrl = [ - config.specifiedByUrl, - ...extensions.map(getSpecifiedByUrl), - ].filter(Boolean)[0]; + let specifiedByUrl = config.specifiedByUrl; + for (const extensionNode of extensions) { + specifiedByUrl = getSpecifiedByUrl(extensionNode) ?? specifiedByUrl; + } return new GraphQLScalarType({ ...config,