From 8bb758db245e5d11cf58180999520a17fed46239 Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Sun, 15 Dec 2019 18:37:08 -0800 Subject: [PATCH] Update `@specified` directive implementation --- docs/APIReference-TypeSystem.md | 1 + src/type/__tests__/definition-test.js | 23 +++++++++++++++++++ src/type/definition.d.ts | 4 +++- src/type/definition.js | 12 ++++++++++ src/type/introspection.js | 16 +++---------- .../__tests__/buildASTSchema-test.js | 19 +++++++++++++++ .../__tests__/buildClientSchema-test.js | 12 ++++++++++ src/utilities/buildASTSchema.js | 9 ++++++++ src/utilities/buildClientSchema.js | 1 + src/utilities/getIntrospectionQuery.d.ts | 1 + src/utilities/getIntrospectionQuery.js | 1 + src/utilities/schemaPrinter.js | 15 +++++++++++- 12 files changed, 99 insertions(+), 15 deletions(-) diff --git a/docs/APIReference-TypeSystem.md b/docs/APIReference-TypeSystem.md index b777db1ad15..7221a1c4f78 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; + specifiedBy?: string; } ``` diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index f589ecab8f3..3b8684d5b2c 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -44,6 +44,16 @@ describe('Type System: Scalars', () => { expect(() => new GraphQLScalarType({ name: 'SomeScalar' })).not.to.throw(); }); + it('accepts a Scalar type defining specifiedBy', () => { + expect( + () => + new GraphQLScalarType({ + name: 'SomeScalar', + specifiedBy: 'https://tools.ietf.org/html/rfc4122', + }), + ).not.to.throw(); + }); + it('accepts a Scalar type defining parseValue and parseLiteral', () => { expect( () => @@ -118,6 +128,19 @@ describe('Type System: Scalars', () => { 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.', ); }); + + it('rejects a Scalar type defining specifiedBy with an incorrect type', () => { + expect( + () => + new GraphQLScalarType({ + name: 'SomeScalar', + // $DisableFlowOnNegativeTest + specifiedBy: {}, + }), + ).to.throw( + 'SomeScalar must provide "specifiedBy" as a string, but got: {}.', + ); + }); }); describe('Type System: Objects', () => { diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index 98789978e68..8536c513443 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -291,6 +291,7 @@ export class GraphQLScalarType { extensions: Maybe>>; astNode: Maybe; extensionASTNodes: Maybe>; + specifiedBy?: Maybe; constructor(config: GraphQLScalarTypeConfig); @@ -300,6 +301,7 @@ export class GraphQLScalarType { parseLiteral: GraphQLScalarLiteralParser; extensions: Maybe>>; extensionASTNodes: ReadonlyArray; + specifiedBy: Maybe; }; toString(): string; @@ -330,6 +332,7 @@ export interface GraphQLScalarTypeConfig { extensions?: Maybe>>; astNode?: Maybe; extensionASTNodes?: Maybe>; + specifiedBy?: Maybe; } /** @@ -409,7 +412,6 @@ export interface GraphQLObjectTypeConfig< > { name: string; description?: Maybe; - specifiedBy?: string; interfaces?: Thunk>; fields: Thunk>; isTypeOf?: Maybe>; diff --git a/src/type/definition.js b/src/type/definition.js index f7ea7b520c7..8454c15bfb2 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -552,6 +552,7 @@ export class GraphQLScalarType { extensions: ?ReadOnlyObjMap; astNode: ?ScalarTypeDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; + specifiedBy: ?string; constructor(config: GraphQLScalarTypeConfig<*, *>): void { const parseValue = config.parseValue || identityFunc; @@ -564,6 +565,7 @@ export class GraphQLScalarType { this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); + this.specifiedBy = config.specifiedBy; devAssert(typeof config.name === 'string', 'Must provide name.'); devAssert( @@ -578,6 +580,14 @@ export class GraphQLScalarType { `${this.name} must provide both "parseValue" and "parseLiteral" functions.`, ); } + + if (config.specifiedBy) { + devAssert( + typeof config.specifiedBy === 'string', + `${this.name} must provide "specifiedBy" as a string, ` + + `but got: ${inspect(config.specifiedBy)}.`, + ); + } } toConfig(): {| @@ -587,6 +597,7 @@ export class GraphQLScalarType { parseLiteral: GraphQLScalarLiteralParser<*>, extensions: ?ReadOnlyObjMap, extensionASTNodes: ?$ReadOnlyArray, + specifiedBy: ?string, |} { return { name: this.name, @@ -597,6 +608,7 @@ export class GraphQLScalarType { extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, + specifiedBy: this.specifiedBy, }; } diff --git a/src/type/introspection.js b/src/type/introspection.js index 22a78abaec7..9c09d5290ad 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -8,10 +8,9 @@ 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, GraphQLSpecifiedDirective } from './directives'; +import { type GraphQLDirective } from './directives'; import { GraphQLString, GraphQLBoolean } from './scalars'; import { type GraphQLType, @@ -224,17 +223,8 @@ export const __Type = new GraphQLObjectType({ }, specifiedBy: { type: GraphQLString, - resolve: type => { - if (!isScalarType(type) || !type.astNode) { - return null; - } - - const specified = getDirectiveValues( - GraphQLSpecifiedDirective, - type.astNode, - ); - return specified && specified.by; - }, + resolve: obj => + obj.specifiedBy !== undefined ? obj.specifiedBy : undefined, }, fields: { type: GraphQLList(GraphQLNonNull(__Field)), diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 1b64534b92f..f2242571d43 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -746,6 +746,25 @@ describe('Schema Builder', () => { }); }); + it('Supports @specified', () => { + const sdl = dedent` + type Query { + someUUID: UUID @deprecated + } + + scalar UUID @specified(by: "https://tools.ietf.org/html/rfc4122") + `; + expect(cycleSDL(sdl)).to.equal(sdl); + + const schema = buildSchema(sdl); + + const uuid = assertScalarType(schema.getType('UUID')); + + expect(uuid).to.include({ + specifiedBy: 'https://tools.ietf.org/html/rfc4122', + }); + }); + it('Correctly assign AST nodes', () => { const sdl = dedent` schema { diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index c3abae4536f..bd737fbf38f 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -530,6 +530,18 @@ describe('Type System: build schema from introspection', () => { expect(cycleIntrospection(sdl)).to.equal(sdl); }); + it('builds a schema with specified by', () => { + const sdl = dedent` + type Query { + someUUID: UUID + } + + scalar UUID @specified(by: "https://tools.ietf.org/html/rfc4122") + `; + + expect(cycleIntrospection(sdl)).to.equal(sdl); + }); + it('can use client schema for limited execution', () => { const schema = buildSchema(` scalar CustomScalar diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 414c8d5c736..325cf73d788 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -446,6 +446,7 @@ export class ASTDefinitionBuilder { name, description, astNode, + specifiedBy: getSpecifiedBy(astNode), }); case Kind.INPUT_OBJECT_TYPE_DEFINITION: return new GraphQLInputObjectType({ @@ -475,6 +476,14 @@ function getDeprecationReason( return deprecated && (deprecated.reason: any); } +/** + * Given a scalar node, returns the string value for specifiedBy string. + */ +function getSpecifiedBy(node: ScalarTypeDefinitionNode): ?string { + const specified = getDirectiveValues(GraphQLSpecifiedDirective, node); + return specified && (specified.by: 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/buildClientSchema.js b/src/utilities/buildClientSchema.js index 45c96b2bc19..b13820aa3b4 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -229,6 +229,7 @@ export function buildClientSchema( return new GraphQLScalarType({ name: scalarIntrospection.name, description: scalarIntrospection.description, + specifiedBy: scalarIntrospection.specifiedBy, }); } diff --git a/src/utilities/getIntrospectionQuery.d.ts b/src/utilities/getIntrospectionQuery.d.ts index 527d946d5d0..bf5cd4a4e52 100644 --- a/src/utilities/getIntrospectionQuery.d.ts +++ b/src/utilities/getIntrospectionQuery.d.ts @@ -49,6 +49,7 @@ export interface IntrospectionScalarType { readonly kind: 'SCALAR'; readonly name: string; readonly description?: Maybe; + readonly specifiedBy?: Maybe; } export interface IntrospectionObjectType { diff --git a/src/utilities/getIntrospectionQuery.js b/src/utilities/getIntrospectionQuery.js index c511cd358d2..8938bbca7b0 100644 --- a/src/utilities/getIntrospectionQuery.js +++ b/src/utilities/getIntrospectionQuery.js @@ -142,6 +142,7 @@ export type IntrospectionScalarType = {| +kind: 'SCALAR', +name: string, +description?: ?string, + +specifiedBy: ?string, |}; export type IntrospectionObjectType = {| diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index 8e306614b54..263ace40d26 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -177,7 +177,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}` + + printSpecifiedBy(type) + ); } function printImplementedInterfaces( @@ -317,6 +321,15 @@ function printDeprecated(fieldOrEnumVal) { return ' @deprecated'; } +function printSpecifiedBy(scalar) { + if (!scalar.specifiedBy) { + return ''; + } + const by = scalar.specifiedBy; + const byAST = astFromValue(by, GraphQLString); + return ' @specified(by: ' + print(byAST) + ')'; +} + function printDescription( options, def,