Skip to content

Commit

Permalink
Add @specifiedBy directive (#2276)
Browse files Browse the repository at this point in the history
Co-Authored-By: christopher butcher <chrisbutcher@users.noreply.github.com>
  • Loading branch information
m14t and chrisbutcher authored May 7, 2020
1 parent 122b305 commit 9d4b433
Show file tree
Hide file tree
Showing 20 changed files with 287 additions and 11 deletions.
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.
*/
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

0 comments on commit 9d4b433

Please sign in to comment.