Skip to content

Commit 9d4b433

Browse files
m14tchrisbutcher
andauthored
Add @specifiedBy directive (#2276)
Co-Authored-By: christopher butcher <[email protected]>
1 parent 122b305 commit 9d4b433

20 files changed

+287
-11
lines changed

docs/APIReference-TypeSystem.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ class GraphQLScalarType<InternalType> {
206206
type GraphQLScalarTypeConfig<InternalType> = {
207207
name: string;
208208
description?: ?string;
209+
specifiedByUrl?: string;
209210
serialize: (value: mixed) => ?InternalType;
210211
parseValue?: (value: mixed) => ?InternalType;
211212
parseLiteral?: (valueAST: Value) => ?InternalType;

src/type/__tests__/definition-test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ describe('Type System: Scalars', () => {
4949
expect(() => new GraphQLScalarType({ name: 'SomeScalar' })).to.not.throw();
5050
});
5151

52+
it('accepts a Scalar type defining specifiedByUrl', () => {
53+
expect(
54+
() =>
55+
new GraphQLScalarType({
56+
name: 'SomeScalar',
57+
specifiedByUrl: 'https://example.com/foo_spec',
58+
}),
59+
).not.to.throw();
60+
});
61+
5262
it('accepts a Scalar type defining parseValue and parseLiteral', () => {
5363
expect(
5464
() =>
@@ -128,6 +138,19 @@ describe('Type System: Scalars', () => {
128138
'SomeScalar must provide both "parseValue" and "parseLiteral" functions.',
129139
);
130140
});
141+
142+
it('rejects a Scalar type defining specifiedByUrl with an incorrect type', () => {
143+
expect(
144+
() =>
145+
new GraphQLScalarType({
146+
name: 'SomeScalar',
147+
// $DisableFlowOnNegativeTest
148+
specifiedByUrl: {},
149+
}),
150+
).to.throw(
151+
'SomeScalar must provide "specifiedByUrl" as a string, but got: {}.',
152+
);
153+
});
131154
});
132155

133156
describe('Type System: Objects', () => {

src/type/__tests__/introspection-test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('Introspection', () => {
3030
});
3131
const source = getIntrospectionQuery({
3232
descriptions: false,
33+
specifiedByUrl: true,
3334
directiveIsRepeatable: true,
3435
});
3536

@@ -46,6 +47,7 @@ describe('Introspection', () => {
4647
{
4748
kind: 'OBJECT',
4849
name: 'QueryRoot',
50+
specifiedByUrl: null,
4951
fields: [
5052
{
5153
name: 'onlyField',
@@ -67,6 +69,7 @@ describe('Introspection', () => {
6769
{
6870
kind: 'SCALAR',
6971
name: 'String',
72+
specifiedByUrl: null,
7073
fields: null,
7174
inputFields: null,
7275
interfaces: null,
@@ -76,6 +79,7 @@ describe('Introspection', () => {
7679
{
7780
kind: 'SCALAR',
7881
name: 'Boolean',
82+
specifiedByUrl: null,
7983
fields: null,
8084
inputFields: null,
8185
interfaces: null,
@@ -85,6 +89,7 @@ describe('Introspection', () => {
8589
{
8690
kind: 'OBJECT',
8791
name: '__Schema',
92+
specifiedByUrl: null,
8893
fields: [
8994
{
9095
name: 'description',
@@ -189,6 +194,7 @@ describe('Introspection', () => {
189194
{
190195
kind: 'OBJECT',
191196
name: '__Type',
197+
specifiedByUrl: null,
192198
fields: [
193199
{
194200
name: 'kind',
@@ -227,6 +233,17 @@ describe('Introspection', () => {
227233
isDeprecated: false,
228234
deprecationReason: null,
229235
},
236+
{
237+
name: 'specifiedByUrl',
238+
args: [],
239+
type: {
240+
kind: 'SCALAR',
241+
name: 'String',
242+
ofType: null,
243+
},
244+
isDeprecated: false,
245+
deprecationReason: null,
246+
},
230247
{
231248
name: 'fields',
232249
args: [
@@ -362,6 +379,7 @@ describe('Introspection', () => {
362379
{
363380
kind: 'ENUM',
364381
name: '__TypeKind',
382+
specifiedByUrl: null,
365383
fields: null,
366384
inputFields: null,
367385
interfaces: null,
@@ -412,6 +430,7 @@ describe('Introspection', () => {
412430
{
413431
kind: 'OBJECT',
414432
name: '__Field',
433+
specifiedByUrl: null,
415434
fields: [
416435
{
417436
name: 'name',
@@ -512,6 +531,7 @@ describe('Introspection', () => {
512531
{
513532
kind: 'OBJECT',
514533
name: '__InputValue',
534+
specifiedByUrl: null,
515535
fields: [
516536
{
517537
name: 'name',
@@ -574,6 +594,7 @@ describe('Introspection', () => {
574594
{
575595
kind: 'OBJECT',
576596
name: '__EnumValue',
597+
specifiedByUrl: null,
577598
fields: [
578599
{
579600
name: 'name',
@@ -636,6 +657,7 @@ describe('Introspection', () => {
636657
{
637658
kind: 'OBJECT',
638659
name: '__Directive',
660+
specifiedByUrl: null,
639661
fields: [
640662
{
641663
name: 'name',
@@ -733,6 +755,7 @@ describe('Introspection', () => {
733755
{
734756
kind: 'ENUM',
735757
name: '__DirectiveLocation',
758+
specifiedByUrl: null,
736759
fields: null,
737760
inputFields: null,
738761
interfaces: null,
@@ -893,6 +916,26 @@ describe('Introspection', () => {
893916
},
894917
],
895918
},
919+
{
920+
name: 'specifiedBy',
921+
isRepeatable: false,
922+
locations: ['SCALAR'],
923+
args: [
924+
{
925+
defaultValue: null,
926+
name: 'url',
927+
type: {
928+
kind: 'NON_NULL',
929+
name: null,
930+
ofType: {
931+
kind: 'SCALAR',
932+
name: 'String',
933+
ofType: null,
934+
},
935+
},
936+
},
937+
],
938+
},
896939
],
897940
},
898941
},

src/type/definition.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ export type Thunk<T> = (() => T) | T;
291291
export class GraphQLScalarType {
292292
name: string;
293293
description: Maybe<string>;
294+
specifiedByUrl: Maybe<string>;
294295
serialize: GraphQLScalarSerializer<any>;
295296
parseValue: GraphQLScalarValueParser<any>;
296297
parseLiteral: GraphQLScalarLiteralParser<any>;
@@ -301,6 +302,7 @@ export class GraphQLScalarType {
301302
constructor(config: Readonly<GraphQLScalarTypeConfig<any, any>>);
302303

303304
toConfig(): GraphQLScalarTypeConfig<any, any> & {
305+
specifiedByUrl: Maybe<string>;
304306
serialize: GraphQLScalarSerializer<any>;
305307
parseValue: GraphQLScalarValueParser<any>;
306308
parseLiteral: GraphQLScalarLiteralParser<any>;
@@ -327,6 +329,7 @@ export type GraphQLScalarLiteralParser<TInternal> = (
327329
export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
328330
name: string;
329331
description?: Maybe<string>;
332+
specifiedByUrl?: Maybe<string>;
330333
// Serializes an internal value to include in a response.
331334
serialize: GraphQLScalarSerializer<TExternal>;
332335
// Parses an externally provided value to use as an input.

src/type/definition.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ function undefineIfEmpty<T>(arr: ?$ReadOnlyArray<T>): ?$ReadOnlyArray<T> {
568568
export class GraphQLScalarType {
569569
name: string;
570570
description: ?string;
571+
specifiedByUrl: ?string;
571572
serialize: GraphQLScalarSerializer<mixed>;
572573
parseValue: GraphQLScalarValueParser<mixed>;
573574
parseLiteral: GraphQLScalarLiteralParser<mixed>;
@@ -579,6 +580,7 @@ export class GraphQLScalarType {
579580
const parseValue = config.parseValue ?? identityFunc;
580581
this.name = config.name;
581582
this.description = config.description;
583+
this.specifiedByUrl = config.specifiedByUrl;
582584
this.serialize = config.serialize ?? identityFunc;
583585
this.parseValue = parseValue;
584586
this.parseLiteral =
@@ -588,6 +590,14 @@ export class GraphQLScalarType {
588590
this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes);
589591

590592
devAssert(typeof config.name === 'string', 'Must provide name.');
593+
594+
devAssert(
595+
config.specifiedByUrl == null ||
596+
typeof config.specifiedByUrl === 'string',
597+
`${this.name} must provide "specifiedByUrl" as a string, ` +
598+
`but got: ${inspect(config.specifiedByUrl)}.`,
599+
);
600+
591601
devAssert(
592602
config.serialize == null || typeof config.serialize === 'function',
593603
`${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.`,
@@ -613,6 +623,7 @@ export class GraphQLScalarType {
613623
return {
614624
name: this.name,
615625
description: this.description,
626+
specifiedByUrl: this.specifiedByUrl,
616627
serialize: this.serialize,
617628
parseValue: this.parseValue,
618629
parseLiteral: this.parseLiteral,
@@ -650,6 +661,7 @@ export type GraphQLScalarLiteralParser<TInternal> = (
650661
export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
651662
name: string,
652663
description?: ?string,
664+
specifiedByUrl?: ?string,
653665
// Serializes an internal value to include in a response.
654666
serialize?: GraphQLScalarSerializer<TExternal>,
655667
// Parses an externally provided value to use as an input.

src/type/directives.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export const GraphQLIncludeDirective: GraphQLDirective;
5959
*/
6060
export const GraphQLSkipDirective: GraphQLDirective;
6161

62+
/**
63+
* Used to provide a URL for specifying the behavior of custom scalar definitions.
64+
*/
65+
export const GraphQLSpecifiedByDirective: GraphQLDirective;
66+
6267
/**
6368
* Constant string used for default reason for a deprecation.
6469
*/

src/type/directives.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,29 @@ export const GraphQLDeprecatedDirective = new GraphQLDirective({
192192
},
193193
});
194194

195+
/**
196+
* Used to provide a URL for specifying the behaviour of custom scalar definitions.
197+
*/
198+
export const GraphQLSpecifiedByDirective = new GraphQLDirective({
199+
name: 'specifiedBy',
200+
description: 'Exposes a URL that specifies the behaviour of this scalar.',
201+
locations: [DirectiveLocation.SCALAR],
202+
args: {
203+
url: {
204+
type: GraphQLNonNull(GraphQLString),
205+
description: 'The URL that specifies the behaviour of this scalar.',
206+
},
207+
},
208+
});
209+
195210
/**
196211
* The full list of specified directives.
197212
*/
198213
export const specifiedDirectives = Object.freeze([
199214
GraphQLIncludeDirective,
200215
GraphQLSkipDirective,
201216
GraphQLDeprecatedDirective,
217+
GraphQLSpecifiedByDirective,
202218
]);
203219

204220
export function isSpecifiedDirective(

src/type/introspection.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export const __DirectiveLocation = new GraphQLEnumType({
195195
export const __Type = new GraphQLObjectType({
196196
name: '__Type',
197197
description:
198-
'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.',
198+
'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.',
199199
fields: () =>
200200
({
201201
kind: {
@@ -239,6 +239,11 @@ export const __Type = new GraphQLObjectType({
239239
resolve: (type) =>
240240
type.description !== undefined ? type.description : undefined,
241241
},
242+
specifiedByUrl: {
243+
type: GraphQLString,
244+
resolve: (obj) =>
245+
obj.specifiedByUrl !== undefined ? obj.specifiedByUrl : undefined,
246+
},
242247
fields: {
243248
type: GraphQLList(GraphQLNonNull(__Field)),
244249
args: {

0 commit comments

Comments
 (0)