Skip to content

Commit

Permalink
Add @specified directive
Browse files Browse the repository at this point in the history
This in an implementation for a spec proposal:

* Spec proposal: [[RFC] Custom Scalar Specification URIs](graphql/graphql-spec#649)
* Original issue: [[RFC] Custom Scalar Specification URIs](graphql/graphql-spec#635)
  • Loading branch information
m14t committed Dec 6, 2019
1 parent 61a07b1 commit 062ccc4
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 10 deletions.
41 changes: 41 additions & 0 deletions src/type/__tests__/introspection-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'SCALAR',
Expand All @@ -70,6 +71,7 @@ describe('Introspection', () => {
interfaces: null,
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -163,6 +165,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -205,6 +208,17 @@ describe('Introspection', () => {
isDeprecated: false,
deprecationReason: null,
},
{
args: [],
deprecationReason: null,
isDeprecated: false,
name: 'specifiedBy',
type: {
kind: 'SCALAR',
name: 'String',
ofType: null,
},
},
{
name: 'fields',
args: [
Expand Down Expand Up @@ -336,6 +350,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'ENUM',
Expand Down Expand Up @@ -386,6 +401,7 @@ describe('Introspection', () => {
},
],
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'SCALAR',
Expand All @@ -395,6 +411,7 @@ describe('Introspection', () => {
interfaces: null,
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -495,6 +512,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -557,6 +575,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -619,6 +638,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'OBJECT',
Expand Down Expand Up @@ -701,6 +721,7 @@ describe('Introspection', () => {
interfaces: [],
enumValues: null,
possibleTypes: null,
specifiedBy: null,
},
{
kind: 'ENUM',
Expand Down Expand Up @@ -806,6 +827,7 @@ describe('Introspection', () => {
},
],
possibleTypes: null,
specifiedBy: null,
},
],
directives: [
Expand Down Expand Up @@ -847,6 +869,25 @@ describe('Introspection', () => {
},
],
},
{
name: 'specified',
locations: ['SCALAR'],
args: [
{
defaultValue: null,
name: 'by',
type: {
kind: 'NON_NULL',
name: null,
ofType: {
kind: 'SCALAR',
name: 'String',
ofType: null,
},
},
},
],
},
{
name: 'deprecated',
locations: ['FIELD_DEFINITION', 'ENUM_VALUE'],
Expand Down
1 change: 1 addition & 0 deletions src/type/definition.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ export interface GraphQLObjectTypeConfig<
> {
name: string;
description?: Maybe<string>;
specifiedBy?: string;
interfaces?: Thunk<Maybe<GraphQLInterfaceType[]>>;
fields: Thunk<GraphQLFieldConfigMap<TSource, TContext, TArgs>>;
isTypeOf?: Maybe<GraphQLIsTypeOfFn<TSource, TContext>>;
Expand Down
6 changes: 6 additions & 0 deletions src/type/directives.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export const GraphQLIncludeDirective: GraphQLDirective;
*/
export const GraphQLSkipDirective: GraphQLDirective;

/**
* Used to provide an RFC3986-compliant URI for specifying the behaviour of
* custom scalar definitions.
*/
export const GraphQLSpecifiedDirective: GraphQLDirective;

/**
* Constant string used for default reason for a deprecation.
*/
Expand Down
16 changes: 16 additions & 0 deletions src/type/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,21 @@ export const GraphQLSkipDirective = new GraphQLDirective({
},
});

/**
* Used to provide URL for specifying the behaviour of custom scalar definitions.
*/
export const GraphQLSpecifiedDirective = new GraphQLDirective({
name: 'specified',
description: 'Exposes a URL that specifies the behaviour of this scalar.',
locations: [DirectiveLocation.SCALAR],
args: {
by: {
type: GraphQLNonNull(GraphQLString),
description: 'The URL that specifies the behaviour of this scalar.',
},
},
});

/**
* Constant string used for default reason for a deprecation.
*/
Expand Down Expand Up @@ -195,6 +210,7 @@ export const GraphQLDeprecatedDirective = new GraphQLDirective({
export const specifiedDirectives = Object.freeze([
GraphQLIncludeDirective,
GraphQLSkipDirective,
GraphQLSpecifiedDirective,
GraphQLDeprecatedDirective,
]);

Expand Down
19 changes: 17 additions & 2 deletions src/type/introspection.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import invariant from '../jsutils/invariant';
import { print } from '../language/printer';
import { DirectiveLocation } from '../language/directiveLocation';
import { astFromValue } from '../utilities/astFromValue';
import { getDirectiveValues } from '../execution/values';

import { type GraphQLSchema } from './schema';
import { type GraphQLDirective } from './directives';
import { type GraphQLDirective, GraphQLSpecifiedDirective } from './directives';
import { GraphQLString, GraphQLBoolean } from './scalars';
import {
type GraphQLType,
Expand Down Expand Up @@ -184,7 +185,7 @@ export const __DirectiveLocation = new GraphQLEnumType({
export const __Type = new GraphQLObjectType({
name: '__Type',
description:
'The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.',
'The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional specifiedBy URL, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.',
fields: () =>
({
kind: {
Expand Down Expand Up @@ -221,6 +222,20 @@ export const __Type = new GraphQLObjectType({
resolve: obj =>
obj.description !== undefined ? obj.description : undefined,
},
specifiedBy: {
type: GraphQLString,
resolve: type => {
if (!isScalarType(type) || !type.astNode) {
return null;
}

const specified = getDirectiveValues(
GraphQLSpecifiedDirective,
type.astNode,
);
return specified && specified.by;
},
},
fields: {
type: GraphQLList(GraphQLNonNull(__Field)),
args: {
Expand Down
19 changes: 14 additions & 5 deletions src/utilities/__tests__/buildASTSchema-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
assertDirective,
GraphQLSkipDirective,
GraphQLIncludeDirective,
GraphQLSpecifiedDirective,
GraphQLDeprecatedDirective,
} from '../../type/directives';
import {
Expand Down Expand Up @@ -182,12 +183,15 @@ describe('Schema Builder', () => {
expect(cycleSDL(sdl, { commentDescriptions: true })).to.equal(sdl);
});

it('Maintains @skip & @include', () => {
it('Maintains @include, @skip & @specified', () => {
const schema = buildSchema('type Query');

expect(schema.getDirectives()).to.have.lengthOf(3);
expect(schema.getDirectives()).to.have.lengthOf(4);
expect(schema.getDirective('skip')).to.equal(GraphQLSkipDirective);
expect(schema.getDirective('include')).to.equal(GraphQLIncludeDirective);
expect(schema.getDirective('specified')).to.equal(
GraphQLSpecifiedDirective,
);
expect(schema.getDirective('deprecated')).to.equal(
GraphQLDeprecatedDirective,
);
Expand All @@ -197,27 +201,32 @@ describe('Schema Builder', () => {
const schema = buildSchema(`
directive @skip on FIELD
directive @include on FIELD
directive @specified on FIELD_DEFINITION
directive @deprecated on FIELD_DEFINITION
`);

expect(schema.getDirectives()).to.have.lengthOf(3);
expect(schema.getDirectives()).to.have.lengthOf(4);
expect(schema.getDirective('skip')).to.not.equal(GraphQLSkipDirective);
expect(schema.getDirective('include')).to.not.equal(
GraphQLIncludeDirective,
);
expect(schema.getDirective('specified')).to.not.equal(
GraphQLSpecifiedDirective,
);
expect(schema.getDirective('deprecated')).to.not.equal(
GraphQLDeprecatedDirective,
);
});

it('Adding directives maintains @skip & @include', () => {
it('Adding directives maintains @include, @skip & @specified', () => {
const schema = buildSchema(`
directive @foo(arg: Int) on FIELD
`);

expect(schema.getDirectives()).to.have.lengthOf(4);
expect(schema.getDirectives()).to.have.lengthOf(5);
expect(schema.getDirective('skip')).to.not.equal(undefined);
expect(schema.getDirective('include')).to.not.equal(undefined);
expect(schema.getDirective('specified')).to.not.equal(undefined);
expect(schema.getDirective('deprecated')).to.not.equal(undefined);
});

Expand Down
7 changes: 6 additions & 1 deletion src/utilities/__tests__/findBreakingChanges-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GraphQLSchema } from '../../type/schema';
import {
GraphQLSkipDirective,
GraphQLIncludeDirective,
GraphQLSpecifiedDirective,
GraphQLDeprecatedDirective,
} from '../../type/directives';

Expand Down Expand Up @@ -790,7 +791,11 @@ describe('findBreakingChanges', () => {
const oldSchema = new GraphQLSchema({});

const newSchema = new GraphQLSchema({
directives: [GraphQLSkipDirective, GraphQLIncludeDirective],
directives: [
GraphQLSkipDirective,
GraphQLIncludeDirective,
GraphQLSpecifiedDirective,
],
});

expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([
Expand Down
18 changes: 16 additions & 2 deletions src/utilities/__tests__/schemaPrinter-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,12 @@ describe('Type System Printer', () => {
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"""Exposes a URL that specifies the behaviour of this scalar."""
directive @specified(
"""The URL that specifies the behaviour of this scalar."""
by: String!
) on SCALAR
"""Marks an element of a GraphQL schema as no longer supported."""
directive @deprecated(
"""
Expand Down Expand Up @@ -735,12 +741,13 @@ describe('Type System Printer', () => {
"""
The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the \`__TypeKind\` enum.
Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.
Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional specifiedBy URL, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.
"""
type __Type {
kind: __TypeKind!
name: String
description: String
specifiedBy: String
fields(includeDeprecated: Boolean = false): [__Field!]
interfaces: [__Type!]
possibleTypes: [__Type!]
Expand Down Expand Up @@ -803,6 +810,12 @@ describe('Type System Printer', () => {
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
# Exposes a URL that specifies the behaviour of this scalar.
directive @specified(
# The URL that specifies the behaviour of this scalar.
by: String!
) on SCALAR
# Marks an element of a GraphQL schema as no longer supported.
directive @deprecated(
# Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/).
Expand Down Expand Up @@ -927,11 +940,12 @@ describe('Type System Printer', () => {
# The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the \`__TypeKind\` enum.
#
# Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.
# Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional specifiedBy URL, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.
type __Type {
kind: __TypeKind!
name: String
description: String
specifiedBy: String
fields(includeDeprecated: Boolean = false): [__Field!]
interfaces: [__Type!]
possibleTypes: [__Type!]
Expand Down
5 changes: 5 additions & 0 deletions src/utilities/buildASTSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
GraphQLSkipDirective,
GraphQLIncludeDirective,
GraphQLDeprecatedDirective,
GraphQLSpecifiedDirective,
} from '../type/directives';
import {
type GraphQLType,
Expand Down Expand Up @@ -170,6 +171,10 @@ export function buildASTSchema(
directives.push(GraphQLIncludeDirective);
}

if (!directives.some(directive => directive.name === 'specified')) {
directives.push(GraphQLSpecifiedDirective);
}

if (!directives.some(directive => directive.name === 'deprecated')) {
directives.push(GraphQLDeprecatedDirective);
}
Expand Down
1 change: 1 addition & 0 deletions src/utilities/getIntrospectionQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string {
kind
name
${descriptions ? 'description' : ''}
specifiedBy
fields(includeDeprecated: true) {
name
${descriptions ? 'description' : ''}
Expand Down

0 comments on commit 062ccc4

Please sign in to comment.