From 847541fd93ff7fad8600496e0762e0c672d93b55 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Fri, 6 May 2016 17:14:37 -0700 Subject: [PATCH] Deprecated directive This adds a new directive as part of the experimental schema language: ``` directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE ``` It also adds support for this directive in the schemaPrinter and buildASTSchema. Additionally exports a new helper `specifiedDirectives` which is encoured to be used when addressing the collection of all directives defined by the spec. The `@deprecated` directive is optimistically added to this collection. While it's currently experimental, it will become part of the schema definition language RFC. --- src/execution/values.js | 2 +- src/index.js | 7 ++- src/type/directives.js | 43 +++++++++++++++++-- src/type/index.js | 7 ++- src/type/schema.js | 34 ++++++--------- .../__tests__/buildASTSchema-test.js | 39 +++++++++++++++-- src/utilities/__tests__/schemaPrinter-test.js | 11 +++-- src/utilities/buildASTSchema.js | 36 +++++++++++++++- src/utilities/schemaPrinter.js | 28 ++++++++++-- 9 files changed, 168 insertions(+), 39 deletions(-) diff --git a/src/execution/values.js b/src/execution/values.js index 4447d13dc64..1a837b75acb 100644 --- a/src/execution/values.js +++ b/src/execution/values.js @@ -54,7 +54,7 @@ export function getVariableValues( export function getArgumentValues( argDefs: ?Array, argASTs: ?Array, - variableValues: { [key: string]: mixed } + variableValues?: ?{ [key: string]: mixed } ): { [key: string]: mixed } { if (!argDefs || !argASTs) { return {}; diff --git a/src/index.js b/src/index.js index 016a5f581c0..cc521c3a3a4 100644 --- a/src/index.js +++ b/src/index.js @@ -66,9 +66,14 @@ export { GraphQLBoolean, GraphQLID, - // Built-in Directives + // Specified Directives + specifiedDirectives, GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLDeprecatedDirective, + + // Constant Deprecation Reason + DEFAULT_DEPRECATION_REASON, // Meta-field definitions. SchemaMetaFieldDef, diff --git a/src/type/directives.js b/src/type/directives.js index ff36829b9c3..9262ea22e13 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -13,7 +13,7 @@ import type { GraphQLFieldConfigArgumentMap, GraphQLArgument } from './definition'; -import { GraphQLBoolean } from './scalars'; +import { GraphQLString, GraphQLBoolean } from './scalars'; import invariant from '../jsutils/invariant'; import { assertValidName } from '../utilities/assertValidName'; @@ -99,7 +99,7 @@ type GraphQLDirectiveConfig = { } /** - * Used to conditionally include fields or fragments + * Used to conditionally include fields or fragments. */ export const GraphQLIncludeDirective = new GraphQLDirective({ name: 'include', @@ -120,7 +120,7 @@ export const GraphQLIncludeDirective = new GraphQLDirective({ }); /** - * Used to conditionally skip (exclude) fields or fragments + * Used to conditionally skip (exclude) fields or fragments. */ export const GraphQLSkipDirective = new GraphQLDirective({ name: 'skip', @@ -139,3 +139,40 @@ export const GraphQLSkipDirective = new GraphQLDirective({ } }, }); + +/** + * Constant string used for default reason for a deprecation. + */ +export const DEFAULT_DEPRECATION_REASON = 'No longer supported'; + +/** + * Used to declare element of a GraphQL schema as deprecated. + */ +export const GraphQLDeprecatedDirective = new GraphQLDirective({ + name: 'deprecated', + description: + 'Marks an element of a GraphQL schema as no longer supported.', + locations: [ + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.ENUM_VALUE, + ], + args: { + reason: { + type: GraphQLString, + description: + 'Explains why this element was deprecated, usually also including a ' + + 'suggestion for how to access supported similar data. Formatted' + + 'in [Markdown](https://daringfireball.net/projects/markdown/).', + defaultValue: DEFAULT_DEPRECATION_REASON + } + }, +}); + +/** + * The full list of specified directives. + */ +export const specifiedDirectives: Array = [ + GraphQLIncludeDirective, + GraphQLSkipDirective, + GraphQLDeprecatedDirective, +]; diff --git a/src/type/index.js b/src/type/index.js index d9171075c17..9432fb649d9 100644 --- a/src/type/index.js +++ b/src/type/index.js @@ -42,9 +42,14 @@ export { // Directives Definition GraphQLDirective, - // Built-in Directives + // Specified Directives + specifiedDirectives, GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLDeprecatedDirective, + + // Constant Deprecation Reason + DEFAULT_DEPRECATION_REASON, } from './directives'; // Common built-in scalar instances. diff --git a/src/type/schema.js b/src/type/schema.js index a63aee72903..65dee8ac14f 100644 --- a/src/type/schema.js +++ b/src/type/schema.js @@ -17,11 +17,7 @@ import { GraphQLNonNull } from './definition'; import type { GraphQLType, GraphQLAbstractType } from './definition'; -import { - GraphQLDirective, - GraphQLIncludeDirective, - GraphQLSkipDirective -} from './directives'; +import { GraphQLDirective, specifiedDirectives } from './directives'; import { __Schema } from './introspection'; import find from '../jsutils/find'; import invariant from '../jsutils/invariant'; @@ -38,21 +34,20 @@ import { isEqualType, isTypeSubTypeOf } from '../utilities/typeComparators'; * Example: * * const MyAppSchema = new GraphQLSchema({ - * query: MyAppQueryRootType - * mutation: MyAppMutationRootType - * }); + * query: MyAppQueryRootType, + * mutation: MyAppMutationRootType, + * }) * * Note: If an array of `directives` are provided to GraphQLSchema, that will be * the exact list of directives represented and allowed. If `directives` is not - * provided then a default set of the built-in `[ @include, @skip ]` directives - * will be used. If you wish to provide *additional* directives to these - * built-ins, you must explicitly declare them. Example: + * provided then a default set of the specified directives (e.g. @include and + * @skip) will be used. If you wish to provide *additional* directives to these + * specified directives, you must explicitly declare them. Example: * - * directives: [ - * myCustomDirective, - * GraphQLIncludeDirective, - * GraphQLSkipDirective - * ] + * const MyAppSchema = new GraphQLSchema({ + * ... + * directives: specifiedDirectives.concat([ myCustomDirective ]), + * }) * */ export class GraphQLSchema { @@ -104,11 +99,8 @@ export class GraphQLSchema { `Schema directives must be Array if provided but got: ${ config.directives}.` ); - // Provide `@include() and `@skip()` directives by default. - this._directives = config.directives || [ - GraphQLIncludeDirective, - GraphQLSkipDirective - ]; + // Provide specified directives (e.g. @include and @skip) by default. + this._directives = config.directives || specifiedDirectives; // Build type map now to detect any errors within this schema. let initialTypes: Array = [ diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 4c5db4474e7..541cf48a1f9 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -15,6 +15,7 @@ import { buildASTSchema } from '../buildASTSchema'; import { GraphQLSkipDirective, GraphQLIncludeDirective, + GraphQLDeprecatedDirective, } from '../../type/directives'; /** @@ -77,12 +78,15 @@ type Hello { } `; const schema = buildASTSchema(parse(body)); - expect(schema.getDirectives().length).to.equal(2); + expect(schema.getDirectives().length).to.equal(3); expect(schema.getDirective('skip')).to.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.equal(GraphQLIncludeDirective); + expect( + schema.getDirective('deprecated') + ).to.equal(GraphQLDeprecatedDirective); }); - it('Overriding directives excludes built-ins', () => { + it('Overriding directives excludes specified', () => { const body = ` schema { query: Hello @@ -90,17 +94,21 @@ schema { directive @skip on FIELD directive @include on FIELD +directive @deprecated on FIELD_DEFINITION type Hello { str: String } `; const schema = buildASTSchema(parse(body)); - expect(schema.getDirectives().length).to.equal(2); + expect(schema.getDirectives().length).to.equal(3); expect(schema.getDirective('skip')).to.not.equal(GraphQLSkipDirective); expect( schema.getDirective('include') ).to.not.equal(GraphQLIncludeDirective); + expect( + schema.getDirective('deprecated') + ).to.not.equal(GraphQLDeprecatedDirective); }); it('Adding directives maintains @skip & @include', () => { @@ -116,9 +124,10 @@ type Hello { } `; const schema = buildASTSchema(parse(body)); - expect(schema.getDirectives().length).to.equal(3); + expect(schema.getDirectives().length).to.equal(4); expect(schema.getDirective('skip')).to.not.equal(undefined); expect(schema.getDirective('include')).to.not.equal(undefined); + expect(schema.getDirective('deprecated')).to.not.equal(undefined); }); it('Type modifiers', () => { @@ -453,6 +462,28 @@ type Query { } union Union = Concrete +`; + const output = cycleOutput(body); + expect(output).to.equal(body); + }); + + it('Supports @deprecated', () => { + const body = ` +schema { + query: Query +} + +enum MyEnum { + VALUE + OLD_VALUE @deprecated + OTHER_VALUE @deprecated(reason: "Terrible reasons") +} + +type Query { + field1: String @deprecated + field2: Int @deprecated(reason: "Because I said so") + enum: MyEnum +} `; const output = cycleOutput(body); expect(output).to.equal(body); diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index bf8395196b8..b75cbf84312 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-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 describe/it, so ignore in this file. +/* eslint-disable max-len */ + import { describe, it } from 'mocha'; import { expect } from 'chai'; import { printSchema, printIntrospectionSchema } from '../schemaPrinter'; @@ -602,14 +605,16 @@ directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE + type __Directive { name: String! description: String locations: [__DirectiveLocation!]! args: [__InputValue!]! - onOperation: Boolean! - onFragment: Boolean! - onField: Boolean! + onOperation: Boolean! @deprecated(reason: "Use \`locations\`.") + onFragment: Boolean! @deprecated(reason: "Use \`locations\`.") + onField: Boolean! @deprecated(reason: "Use \`locations\`.") } enum __DirectiveLocation { diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 14d8f5f2453..f97f0718047 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -8,11 +8,14 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +import find from '../jsutils/find'; import invariant from '../jsutils/invariant'; import keyMap from '../jsutils/keyMap'; import keyValMap from '../jsutils/keyValMap'; import { valueFromAST } from './valueFromAST'; +import { getArgumentValues } from '../execution/values'; + import { LIST_TYPE, NON_NULL_TYPE, @@ -29,6 +32,7 @@ import { import type { Document, + Directive, Type, NamedType, SchemaDefinition, @@ -64,6 +68,7 @@ import { GraphQLDirective, GraphQLSkipDirective, GraphQLIncludeDirective, + GraphQLDeprecatedDirective, } from '../type/directives'; import { @@ -224,7 +229,7 @@ export function buildASTSchema(ast: Document): GraphQLSchema { const directives = directiveDefs.map(getDirective); - // If skip and include were not explicitly declared, add them. + // If built-in directives were not explicitly declared, add them. if (!directives.some(directive => directive.name === 'skip')) { directives.push(GraphQLSkipDirective); } @@ -233,6 +238,10 @@ export function buildASTSchema(ast: Document): GraphQLSchema { directives.push(GraphQLIncludeDirective); } + if (!directives.some(directive => directive.name === 'deprecated')) { + directives.push(GraphQLDeprecatedDirective); + } + return new GraphQLSchema({ query: getObjectType(astMap[queryTypeName]), mutation: mutationTypeName ? getObjectType(astMap[mutationTypeName]) : null, @@ -321,6 +330,7 @@ export function buildASTSchema(ast: Document): GraphQLSchema { field => ({ type: produceTypeDef(field.type), args: makeInputValues(field.arguments), + deprecationReason: getDeprecationReason(field.directives) }) ); } @@ -353,7 +363,14 @@ export function buildASTSchema(ast: Document): GraphQLSchema { function makeEnumDef(def: EnumTypeDefinition) { const enumType = new GraphQLEnumType({ name: def.name.value, - values: keyValMap(def.values, v => v.name.value, () => ({})), + values: keyValMap( + def.values, + enumValue => enumValue.name.value, + enumValue => ({ + value: enumValue.name.value, + deprecationReason: getDeprecationReason(enumValue.directives) + }) + ), }); return enumType; @@ -387,3 +404,18 @@ export function buildASTSchema(ast: Document): GraphQLSchema { }); } } + +function getDeprecationReason(directives: ?Array): ?string { + const deprecatedAST = directives && find( + directives, + directive => directive.name.value === GraphQLDeprecatedDirective.name + ); + if (!deprecatedAST) { + return; + } + const { reason } = getArgumentValues( + GraphQLDeprecatedDirective.args, + deprecatedAST.arguments + ); + return (reason: any); +} diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index f977f9e4a8e..6f174cc5ae8 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -22,6 +22,10 @@ import { GraphQLEnumType, GraphQLInputObjectType, } from '../type/definition'; +import { + GraphQLDeprecatedDirective, + DEFAULT_DEPRECATION_REASON, +} from '../type/directives'; export function printSchema(schema: GraphQLSchema): string { @@ -33,7 +37,11 @@ export function printIntrospectionSchema(schema: GraphQLSchema): string { } function isSpecDirective(directiveName: string): boolean { - return directiveName === 'skip' || directiveName === 'include'; + return ( + directiveName === 'skip' || + directiveName === 'include' || + directiveName === 'deprecated' + ); } function isDefinedType(typename: string): boolean { @@ -135,7 +143,7 @@ function printUnion(type: GraphQLUnionType): string { function printEnum(type: GraphQLEnumType): string { const values = type.getValues(); return `enum ${type.name} {\n` + - values.map(v => ' ' + v.name).join('\n') + '\n' + + values.map(v => ' ' + v.name + printDeprecated(v)).join('\n') + '\n' + '}'; } @@ -151,10 +159,24 @@ function printFields(type) { const fieldMap = type.getFields(); const fields = Object.keys(fieldMap).map(fieldName => fieldMap[fieldName]); return fields.map( - f => ` ${f.name}${printArgs(f)}: ${f.type}` + f => ` ${f.name}${printArgs(f)}: ${f.type}${printDeprecated(f)}` ).join('\n'); } +function printDeprecated(fieldOrEnumVal) { + const reason = fieldOrEnumVal.deprecationReason; + if (isNullish(reason)) { + return ''; + } + if ( + reason === '' || + reason === DEFAULT_DEPRECATION_REASON + ) { + return ' @deprecated'; + } + return ' @deprecated(reason: ' + print(astFromValue(reason)) + ')'; +} + function printArgs(fieldOrDirectives) { if (fieldOrDirectives.args.length === 0) { return '';