Skip to content

Commit

Permalink
Deprecated directive
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
leebyron committed May 7, 2016
1 parent 0aa78f6 commit 6796fad
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/execution/values.js
Expand Up @@ -54,7 +54,7 @@ export function getVariableValues(
export function getArgumentValues(
argDefs: ?Array<GraphQLArgument>,
argASTs: ?Array<Argument>,
variableValues: { [key: string]: mixed }
variableValues?: ?{ [key: string]: mixed }
): { [key: string]: mixed } {
if (!argDefs || !argASTs) {
return {};
Expand Down
7 changes: 6 additions & 1 deletion src/index.js
Expand Up @@ -66,9 +66,14 @@ export {
GraphQLBoolean,
GraphQLID,

// Built-in Directives
// Built-in Directives defined by the Spec
specifiedDirectives,
GraphQLIncludeDirective,
GraphQLSkipDirective,
GraphQLDeprecatedDirective,

// Constant Deprecation Reason
DEFAULT_DEPRECATION_REASON,

// Meta-field definitions.
SchemaMetaFieldDef,
Expand Down
43 changes: 40 additions & 3 deletions src/type/directives.js
Expand Up @@ -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';

Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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<GraphQLDirective> = [
GraphQLIncludeDirective,
GraphQLSkipDirective,
GraphQLDeprecatedDirective,
];
7 changes: 6 additions & 1 deletion src/type/index.js
Expand Up @@ -42,9 +42,14 @@ export {
// Directives Definition
GraphQLDirective,

// Built-in Directives
// Built-in Directives defined by the Spec
specifiedDirectives,
GraphQLIncludeDirective,
GraphQLSkipDirective,
GraphQLDeprecatedDirective,

// Constant Deprecation Reason
DEFAULT_DEPRECATION_REASON,
} from './directives';

// Common built-in scalar instances.
Expand Down
34 changes: 13 additions & 21 deletions src/type/schema.js
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -104,11 +99,8 @@ export class GraphQLSchema {
`Schema directives must be Array<GraphQLDirective> 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<?GraphQLType> = [
Expand Down
39 changes: 35 additions & 4 deletions src/utilities/__tests__/buildASTSchema-test.js
Expand Up @@ -15,6 +15,7 @@ import { buildASTSchema } from '../buildASTSchema';
import {
GraphQLSkipDirective,
GraphQLIncludeDirective,
GraphQLDeprecatedDirective,
} from '../../type/directives';

/**
Expand Down Expand Up @@ -77,30 +78,37 @@ 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
}
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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 8 additions & 3 deletions src/utilities/__tests__/schemaPrinter-test.js
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
35 changes: 33 additions & 2 deletions src/utilities/buildASTSchema.js
Expand Up @@ -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,
Expand All @@ -29,6 +32,7 @@ import {

import type {
Document,
Directive,
Type,
NamedType,
SchemaDefinition,
Expand Down Expand Up @@ -64,6 +68,7 @@ import {
GraphQLDirective,
GraphQLSkipDirective,
GraphQLIncludeDirective,
GraphQLDeprecatedDirective,
} from '../type/directives';

import {
Expand Down Expand Up @@ -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 specified directives were not explicitly declared, add them.
if (!directives.some(directive => directive.name === 'skip')) {
directives.push(GraphQLSkipDirective);
}
Expand All @@ -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,
Expand Down Expand Up @@ -321,6 +330,7 @@ export function buildASTSchema(ast: Document): GraphQLSchema {
field => ({
type: produceTypeDef(field.type),
args: makeInputValues(field.arguments),
deprecationReason: getDeprecationReason(field.directives)
})
);
}
Expand Down Expand Up @@ -353,7 +363,13 @@ 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 => ({
deprecationReason: getDeprecationReason(enumValue.directives)
})
),
});

return enumType;
Expand Down Expand Up @@ -387,3 +403,18 @@ export function buildASTSchema(ast: Document): GraphQLSchema {
});
}
}

function getDeprecationReason(directives: ?Array<Directive>): ?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);
}

0 comments on commit 6796fad

Please sign in to comment.