Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @specifiedBy directive #2276

Merged
merged 13 commits into from
May 7, 2020
1 change: 1 addition & 0 deletions docs/APIReference-TypeSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ class GraphQLScalarType<InternalType> {
type GraphQLScalarTypeConfig<InternalType> = {
name: string;
description?: ?string;
specifiedByUrl?: string;
serialize: (value: mixed) => ?InternalType;
parseValue?: (value: mixed) => ?InternalType;
parseLiteral?: (valueAST: Value) => ?InternalType;
Expand Down
23 changes: 23 additions & 0 deletions src/type/__tests__/definition-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
() =>
Expand Down Expand Up @@ -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', () => {
Expand Down
43 changes: 43 additions & 0 deletions src/type/__tests__/introspection-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('Introspection', () => {
});
const source = getIntrospectionQuery({
descriptions: false,
specifiedByUrl: true,
directiveIsRepeatable: true,
});

Expand All @@ -46,6 +47,7 @@ describe('Introspection', () => {
{
kind: 'OBJECT',
name: 'QueryRoot',
specifiedByUrl: null,
fields: [
{
name: 'onlyField',
Expand All @@ -67,6 +69,7 @@ describe('Introspection', () => {
{
kind: 'SCALAR',
name: 'String',
specifiedByUrl: null,
fields: null,
inputFields: null,
interfaces: null,
Expand All @@ -76,6 +79,7 @@ describe('Introspection', () => {
{
kind: 'SCALAR',
name: 'Boolean',
specifiedByUrl: null,
fields: null,
inputFields: null,
interfaces: null,
Expand All @@ -85,6 +89,7 @@ describe('Introspection', () => {
{
kind: 'OBJECT',
name: '__Schema',
specifiedByUrl: null,
fields: [
{
name: 'description',
Expand Down Expand Up @@ -189,6 +194,7 @@ describe('Introspection', () => {
{
kind: 'OBJECT',
name: '__Type',
specifiedByUrl: null,
fields: [
{
name: 'kind',
Expand Down Expand Up @@ -227,6 +233,17 @@ describe('Introspection', () => {
isDeprecated: false,
deprecationReason: null,
},
{
name: 'specifiedByUrl',
args: [],
type: {
kind: 'SCALAR',
name: 'String',
ofType: null,
},
isDeprecated: false,
deprecationReason: null,
},
{
name: 'fields',
args: [
Expand Down Expand Up @@ -362,6 +379,7 @@ describe('Introspection', () => {
{
kind: 'ENUM',
name: '__TypeKind',
specifiedByUrl: null,
fields: null,
inputFields: null,
interfaces: null,
Expand Down Expand Up @@ -412,6 +430,7 @@ describe('Introspection', () => {
{
kind: 'OBJECT',
name: '__Field',
specifiedByUrl: null,
fields: [
{
name: 'name',
Expand Down Expand Up @@ -512,6 +531,7 @@ describe('Introspection', () => {
{
kind: 'OBJECT',
name: '__InputValue',
specifiedByUrl: null,
fields: [
{
name: 'name',
Expand Down Expand Up @@ -574,6 +594,7 @@ describe('Introspection', () => {
{
kind: 'OBJECT',
name: '__EnumValue',
specifiedByUrl: null,
fields: [
{
name: 'name',
Expand Down Expand Up @@ -636,6 +657,7 @@ describe('Introspection', () => {
{
kind: 'OBJECT',
name: '__Directive',
specifiedByUrl: null,
fields: [
{
name: 'name',
Expand Down Expand Up @@ -733,6 +755,7 @@ describe('Introspection', () => {
{
kind: 'ENUM',
name: '__DirectiveLocation',
specifiedByUrl: null,
fields: null,
inputFields: null,
interfaces: null,
Expand Down Expand Up @@ -893,6 +916,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,
},
},
},
],
},
],
},
},
Expand Down
3 changes: 3 additions & 0 deletions src/type/definition.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ export type Thunk<T> = (() => T) | T;
export class GraphQLScalarType {
name: string;
description: Maybe<string>;
specifiedByUrl: Maybe<string>;
serialize: GraphQLScalarSerializer<any>;
parseValue: GraphQLScalarValueParser<any>;
parseLiteral: GraphQLScalarLiteralParser<any>;
Expand All @@ -301,6 +302,7 @@ export class GraphQLScalarType {
constructor(config: Readonly<GraphQLScalarTypeConfig<any, any>>);

toConfig(): GraphQLScalarTypeConfig<any, any> & {
specifiedByUrl: Maybe<string>;
serialize: GraphQLScalarSerializer<any>;
parseValue: GraphQLScalarValueParser<any>;
parseLiteral: GraphQLScalarLiteralParser<any>;
Expand All @@ -327,6 +329,7 @@ export type GraphQLScalarLiteralParser<TInternal> = (
export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
name: string;
description?: Maybe<string>;
specifiedByUrl?: Maybe<string>;
// Serializes an internal value to include in a response.
serialize: GraphQLScalarSerializer<TExternal>;
// Parses an externally provided value to use as an input.
Expand Down
12 changes: 12 additions & 0 deletions src/type/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ function undefineIfEmpty<T>(arr: ?$ReadOnlyArray<T>): ?$ReadOnlyArray<T> {
export class GraphQLScalarType {
name: string;
description: ?string;
specifiedByUrl: ?string;
serialize: GraphQLScalarSerializer<mixed>;
parseValue: GraphQLScalarValueParser<mixed>;
parseLiteral: GraphQLScalarLiteralParser<mixed>;
Expand All @@ -579,6 +580,7 @@ export class GraphQLScalarType {
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 =
Expand All @@ -588,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.`,
Expand All @@ -613,6 +623,7 @@ export class GraphQLScalarType {
return {
name: this.name,
description: this.description,
specifiedByUrl: this.specifiedByUrl,
serialize: this.serialize,
parseValue: this.parseValue,
parseLiteral: this.parseLiteral,
Expand Down Expand Up @@ -650,6 +661,7 @@ export type GraphQLScalarLiteralParser<TInternal> = (
export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
name: string,
description?: ?string,
specifiedByUrl?: ?string,
// Serializes an internal value to include in a response.
serialize?: GraphQLScalarSerializer<TExternal>,
// Parses an externally provided value to use as an input.
Expand Down
5 changes: 5 additions & 0 deletions src/type/directives.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
m14t marked this conversation as resolved.
Show resolved Hide resolved
export const GraphQLSpecifiedByDirective: 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 @@ -192,13 +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,
GraphQLDeprecatedDirective,
GraphQLSpecifiedByDirective,
]);

export function isSpecifiedDirective(
Expand Down
7 changes: 6 additions & 1 deletion src/type/introspection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down
Loading