Skip to content

Commit 2bded14

Browse files
committed
Add @specifiedBy directive
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)
1 parent db214ce commit 2bded14

19 files changed

+263
-10
lines changed

docs/APIReference-TypeSystem.md

+1
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ type GraphQLScalarTypeConfig<InternalType> = {
209209
serialize: (value: mixed) => ?InternalType;
210210
parseValue?: (value: mixed) => ?InternalType;
211211
parseLiteral?: (valueAST: Value) => ?InternalType;
212+
specifiedByUrl?: string;
212213
}
213214
```
214215

src/type/__tests__/definition-test.js

+23
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

+42
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ describe('Introspection', () => {
6363
interfaces: [],
6464
enumValues: null,
6565
possibleTypes: null,
66+
specifiedByUrl: null,
6667
},
6768
{
6869
kind: 'SCALAR',
@@ -72,6 +73,7 @@ describe('Introspection', () => {
7273
interfaces: null,
7374
enumValues: null,
7475
possibleTypes: null,
76+
specifiedByUrl: null,
7577
},
7678
{
7779
kind: 'SCALAR',
@@ -81,6 +83,7 @@ describe('Introspection', () => {
8183
interfaces: null,
8284
enumValues: null,
8385
possibleTypes: null,
86+
specifiedByUrl: null,
8487
},
8588
{
8689
kind: 'OBJECT',
@@ -185,6 +188,7 @@ describe('Introspection', () => {
185188
interfaces: [],
186189
enumValues: null,
187190
possibleTypes: null,
191+
specifiedByUrl: null,
188192
},
189193
{
190194
kind: 'OBJECT',
@@ -227,6 +231,17 @@ describe('Introspection', () => {
227231
isDeprecated: false,
228232
deprecationReason: null,
229233
},
234+
{
235+
name: 'specifiedByUrl',
236+
args: [],
237+
type: {
238+
kind: 'SCALAR',
239+
name: 'String',
240+
ofType: null,
241+
},
242+
isDeprecated: false,
243+
deprecationReason: null,
244+
},
230245
{
231246
name: 'fields',
232247
args: [
@@ -358,6 +373,7 @@ describe('Introspection', () => {
358373
interfaces: [],
359374
enumValues: null,
360375
possibleTypes: null,
376+
specifiedByUrl: null,
361377
},
362378
{
363379
kind: 'ENUM',
@@ -408,6 +424,7 @@ describe('Introspection', () => {
408424
},
409425
],
410426
possibleTypes: null,
427+
specifiedByUrl: null,
411428
},
412429
{
413430
kind: 'OBJECT',
@@ -508,6 +525,7 @@ describe('Introspection', () => {
508525
interfaces: [],
509526
enumValues: null,
510527
possibleTypes: null,
528+
specifiedByUrl: null,
511529
},
512530
{
513531
kind: 'OBJECT',
@@ -570,6 +588,7 @@ describe('Introspection', () => {
570588
interfaces: [],
571589
enumValues: null,
572590
possibleTypes: null,
591+
specifiedByUrl: null,
573592
},
574593
{
575594
kind: 'OBJECT',
@@ -632,6 +651,7 @@ describe('Introspection', () => {
632651
interfaces: [],
633652
enumValues: null,
634653
possibleTypes: null,
654+
specifiedByUrl: null,
635655
},
636656
{
637657
kind: 'OBJECT',
@@ -729,6 +749,7 @@ describe('Introspection', () => {
729749
interfaces: [],
730750
enumValues: null,
731751
possibleTypes: null,
752+
specifiedByUrl: null,
732753
},
733754
{
734755
kind: 'ENUM',
@@ -834,6 +855,7 @@ describe('Introspection', () => {
834855
},
835856
],
836857
possibleTypes: null,
858+
specifiedByUrl: null,
837859
},
838860
],
839861
directives: [
@@ -877,6 +899,26 @@ describe('Introspection', () => {
877899
},
878900
],
879901
},
902+
{
903+
name: 'specifiedBy',
904+
isRepeatable: false,
905+
locations: ['SCALAR'],
906+
args: [
907+
{
908+
defaultValue: null,
909+
name: 'url',
910+
type: {
911+
kind: 'NON_NULL',
912+
name: null,
913+
ofType: {
914+
kind: 'SCALAR',
915+
name: 'String',
916+
ofType: null,
917+
},
918+
},
919+
},
920+
],
921+
},
880922
{
881923
name: 'deprecated',
882924
isRepeatable: false,

src/type/definition.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ export class GraphQLScalarType {
292292
extensions: Maybe<Readonly<Record<string, any>>>;
293293
astNode: Maybe<ScalarTypeDefinitionNode>;
294294
extensionASTNodes: Maybe<ReadonlyArray<ScalarTypeExtensionNode>>;
295+
specifiedByUrl?: Maybe<string>;
295296

296297
constructor(config: Readonly<GraphQLScalarTypeConfig<any, any>>);
297298

@@ -301,6 +302,7 @@ export class GraphQLScalarType {
301302
parseLiteral: GraphQLScalarLiteralParser<any>;
302303
extensions: Maybe<Readonly<Record<string, any>>>;
303304
extensionASTNodes: ReadonlyArray<ScalarTypeExtensionNode>;
305+
specifiedByUrl: Maybe<string>;
304306
};
305307

306308
toString(): string;
@@ -331,6 +333,7 @@ export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
331333
extensions?: Maybe<Readonly<Record<string, any>>>;
332334
astNode?: Maybe<ScalarTypeDefinitionNode>;
333335
extensionASTNodes?: Maybe<ReadonlyArray<ScalarTypeExtensionNode>>;
336+
specifiedByUrl?: Maybe<string>;
334337
}
335338

336339
/**

src/type/definition.js

+13
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ export class GraphQLScalarType {
573573
extensions: ?ReadOnlyObjMap<mixed>;
574574
astNode: ?ScalarTypeDefinitionNode;
575575
extensionASTNodes: ?$ReadOnlyArray<ScalarTypeExtensionNode>;
576+
specifiedByUrl: ?string;
576577

577578
constructor(config: $ReadOnly<GraphQLScalarTypeConfig<mixed, mixed>>): void {
578579
const parseValue = config.parseValue ?? identityFunc;
@@ -585,6 +586,7 @@ export class GraphQLScalarType {
585586
this.extensions = config.extensions && toObjMap(config.extensions);
586587
this.astNode = config.astNode;
587588
this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes);
589+
this.specifiedByUrl = config.specifiedByUrl;
588590

589591
devAssert(typeof config.name === 'string', 'Must provide name.');
590592
devAssert(
@@ -599,6 +601,14 @@ export class GraphQLScalarType {
599601
`${this.name} must provide both "parseValue" and "parseLiteral" functions.`,
600602
);
601603
}
604+
605+
if (config.specifiedByUrl != null) {
606+
devAssert(
607+
typeof config.specifiedByUrl === 'string',
608+
`${this.name} must provide "specifiedByUrl" as a string, ` +
609+
`but got: ${inspect(config.specifiedByUrl)}.`,
610+
);
611+
}
602612
}
603613
604614
toConfig(): {|
@@ -608,6 +618,7 @@ export class GraphQLScalarType {
608618
parseLiteral: GraphQLScalarLiteralParser<mixed>,
609619
extensions: ?ReadOnlyObjMap<mixed>,
610620
extensionASTNodes: $ReadOnlyArray<ScalarTypeExtensionNode>,
621+
specifiedByUrl: ?string,
611622
|} {
612623
return {
613624
name: this.name,
@@ -618,6 +629,7 @@ export class GraphQLScalarType {
618629
extensions: this.extensions,
619630
astNode: this.astNode,
620631
extensionASTNodes: this.extensionASTNodes ?? [],
632+
specifiedByUrl: this.specifiedByUrl,
621633
};
622634
}
623635
@@ -658,6 +670,7 @@ export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
658670
extensions?: ?ReadOnlyObjMapLike<mixed>,
659671
astNode?: ?ScalarTypeDefinitionNode,
660672
extensionASTNodes?: ?$ReadOnlyArray<ScalarTypeExtensionNode>,
673+
specifiedByUrl?: ?string,
661674
|};
662675
663676
/**

src/type/directives.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export const GraphQLIncludeDirective: GraphQLDirective;
5656
*/
5757
export const GraphQLSkipDirective: GraphQLDirective;
5858

59+
/**
60+
* Used to provide a URL for specifying the behavior of custom scalar definitions.
61+
*/
62+
export const GraphQLSpecifiedByDirective: GraphQLDirective;
63+
5964
/**
6065
* Constant string used for default reason for a deprecation.
6166
*/

src/type/directives.js

+16
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,21 @@ export const GraphQLSkipDirective = new GraphQLDirective({
170170
},
171171
});
172172

173+
/**
174+
* Used to provide a URL for specifying the behaviour of custom scalar definitions.
175+
*/
176+
export const GraphQLSpecifiedByDirective = new GraphQLDirective({
177+
name: 'specifiedBy',
178+
description: 'Exposes a URL that specifies the behaviour of this scalar.',
179+
locations: [DirectiveLocation.SCALAR],
180+
args: {
181+
url: {
182+
type: GraphQLNonNull(GraphQLString),
183+
description: 'The URL that specifies the behaviour of this scalar.',
184+
},
185+
},
186+
});
187+
173188
/**
174189
* Constant string used for default reason for a deprecation.
175190
*/
@@ -198,6 +213,7 @@ export const GraphQLDeprecatedDirective = new GraphQLDirective({
198213
export const specifiedDirectives = Object.freeze([
199214
GraphQLIncludeDirective,
200215
GraphQLSkipDirective,
216+
GraphQLSpecifiedByDirective,
201217
GraphQLDeprecatedDirective,
202218
]);
203219

src/type/introspection.js

+6-1
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)