Skip to content

Commit 18940cf

Browse files
committed
Add @specified 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 576682f commit 18940cf

19 files changed

+246
-10
lines changed

docs/APIReference-TypeSystem.md

Lines changed: 1 addition & 0 deletions
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+
specifiedBy?: string;
212213
}
213214
```
214215

src/type/__tests__/definition-test.js

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

47+
it('accepts a Scalar type defining specifiedBy', () => {
48+
expect(
49+
() =>
50+
new GraphQLScalarType({
51+
name: 'SomeScalar',
52+
specifiedBy: 'https://tools.ietf.org/html/rfc4122',
53+
}),
54+
).not.to.throw();
55+
});
56+
4757
it('accepts a Scalar type defining parseValue and parseLiteral', () => {
4858
expect(
4959
() =>
@@ -118,6 +128,19 @@ describe('Type System: Scalars', () => {
118128
'SomeScalar must provide both "parseValue" and "parseLiteral" functions.',
119129
);
120130
});
131+
132+
it('rejects a Scalar type defining specifiedBy with an incorrect type', () => {
133+
expect(
134+
() =>
135+
new GraphQLScalarType({
136+
name: 'SomeScalar',
137+
// $DisableFlowOnNegativeTest
138+
specifiedBy: {},
139+
}),
140+
).to.throw(
141+
'SomeScalar must provide "specifiedBy" as a string, but got: {}.',
142+
);
143+
});
121144
});
122145

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

src/type/__tests__/introspection-test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe('Introspection', () => {
5959
interfaces: [],
6060
enumValues: null,
6161
possibleTypes: null,
62+
specifiedBy: null,
6263
},
6364
{
6465
kind: 'SCALAR',
@@ -68,6 +69,7 @@ describe('Introspection', () => {
6869
interfaces: null,
6970
enumValues: null,
7071
possibleTypes: null,
72+
specifiedBy: null,
7173
},
7274
{
7375
kind: 'OBJECT',
@@ -161,6 +163,7 @@ describe('Introspection', () => {
161163
interfaces: [],
162164
enumValues: null,
163165
possibleTypes: null,
166+
specifiedBy: null,
164167
},
165168
{
166169
kind: 'OBJECT',
@@ -203,6 +206,17 @@ describe('Introspection', () => {
203206
isDeprecated: false,
204207
deprecationReason: null,
205208
},
209+
{
210+
args: [],
211+
deprecationReason: null,
212+
isDeprecated: false,
213+
name: 'specifiedBy',
214+
type: {
215+
kind: 'SCALAR',
216+
name: 'String',
217+
ofType: null,
218+
},
219+
},
206220
{
207221
name: 'fields',
208222
args: [
@@ -334,6 +348,7 @@ describe('Introspection', () => {
334348
interfaces: [],
335349
enumValues: null,
336350
possibleTypes: null,
351+
specifiedBy: null,
337352
},
338353
{
339354
kind: 'ENUM',
@@ -384,6 +399,7 @@ describe('Introspection', () => {
384399
},
385400
],
386401
possibleTypes: null,
402+
specifiedBy: null,
387403
},
388404
{
389405
kind: 'SCALAR',
@@ -393,6 +409,7 @@ describe('Introspection', () => {
393409
interfaces: null,
394410
enumValues: null,
395411
possibleTypes: null,
412+
specifiedBy: null,
396413
},
397414
{
398415
kind: 'OBJECT',
@@ -493,6 +510,7 @@ describe('Introspection', () => {
493510
interfaces: [],
494511
enumValues: null,
495512
possibleTypes: null,
513+
specifiedBy: null,
496514
},
497515
{
498516
kind: 'OBJECT',
@@ -555,6 +573,7 @@ describe('Introspection', () => {
555573
interfaces: [],
556574
enumValues: null,
557575
possibleTypes: null,
576+
specifiedBy: null,
558577
},
559578
{
560579
kind: 'OBJECT',
@@ -617,6 +636,7 @@ describe('Introspection', () => {
617636
interfaces: [],
618637
enumValues: null,
619638
possibleTypes: null,
639+
specifiedBy: null,
620640
},
621641
{
622642
kind: 'OBJECT',
@@ -699,6 +719,7 @@ describe('Introspection', () => {
699719
interfaces: [],
700720
enumValues: null,
701721
possibleTypes: null,
722+
specifiedBy: null,
702723
},
703724
{
704725
kind: 'ENUM',
@@ -804,6 +825,7 @@ describe('Introspection', () => {
804825
},
805826
],
806827
possibleTypes: null,
828+
specifiedBy: null,
807829
},
808830
],
809831
directives: [
@@ -845,6 +867,25 @@ describe('Introspection', () => {
845867
},
846868
],
847869
},
870+
{
871+
name: 'specified',
872+
locations: ['SCALAR'],
873+
args: [
874+
{
875+
defaultValue: null,
876+
name: 'by',
877+
type: {
878+
kind: 'NON_NULL',
879+
name: null,
880+
ofType: {
881+
kind: 'SCALAR',
882+
name: 'String',
883+
ofType: null,
884+
},
885+
},
886+
},
887+
],
888+
},
848889
{
849890
name: 'deprecated',
850891
locations: ['FIELD_DEFINITION', 'ENUM_VALUE'],

src/type/definition.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ export class GraphQLScalarType {
291291
extensions: Maybe<Readonly<Record<string, any>>>;
292292
astNode: Maybe<ScalarTypeDefinitionNode>;
293293
extensionASTNodes: Maybe<ReadonlyArray<ScalarTypeExtensionNode>>;
294+
specifiedBy?: Maybe<string>;
294295

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

@@ -300,6 +301,7 @@ export class GraphQLScalarType {
300301
parseLiteral: GraphQLScalarLiteralParser<any>;
301302
extensions: Maybe<Readonly<Record<string, any>>>;
302303
extensionASTNodes: ReadonlyArray<ScalarTypeExtensionNode>;
304+
specifiedBy: Maybe<string>;
303305
};
304306

305307
toString(): string;
@@ -330,6 +332,7 @@ export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
330332
extensions?: Maybe<Readonly<Record<string, any>>>;
331333
astNode?: Maybe<ScalarTypeDefinitionNode>;
332334
extensionASTNodes?: Maybe<ReadonlyArray<ScalarTypeExtensionNode>>;
335+
specifiedBy?: Maybe<string>;
333336
}
334337

335338
/**

src/type/definition.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ export class GraphQLScalarType {
552552
extensions: ?ReadOnlyObjMap<mixed>;
553553
astNode: ?ScalarTypeDefinitionNode;
554554
extensionASTNodes: ?$ReadOnlyArray<ScalarTypeExtensionNode>;
555+
specifiedBy: ?string;
555556

556557
constructor(config: $ReadOnly<GraphQLScalarTypeConfig<mixed, mixed>>): void {
557558
const parseValue = config.parseValue || identityFunc;
@@ -564,6 +565,7 @@ export class GraphQLScalarType {
564565
this.extensions = config.extensions && toObjMap(config.extensions);
565566
this.astNode = config.astNode;
566567
this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes);
568+
this.specifiedBy = config.specifiedBy;
567569

568570
devAssert(typeof config.name === 'string', 'Must provide name.');
569571
devAssert(
@@ -578,6 +580,14 @@ export class GraphQLScalarType {
578580
`${this.name} must provide both "parseValue" and "parseLiteral" functions.`,
579581
);
580582
}
583+
584+
if (config.specifiedBy != null) {
585+
devAssert(
586+
typeof config.specifiedBy === 'string',
587+
`${this.name} must provide "specifiedBy" as a string, ` +
588+
`but got: ${inspect(config.specifiedBy)}.`,
589+
);
590+
}
581591
}
582592
583593
toConfig(): {|
@@ -587,6 +597,7 @@ export class GraphQLScalarType {
587597
parseLiteral: GraphQLScalarLiteralParser<mixed>,
588598
extensions: ?ReadOnlyObjMap<mixed>,
589599
extensionASTNodes: ?$ReadOnlyArray<ScalarTypeExtensionNode>,
600+
specifiedBy: ?string,
590601
|} {
591602
return {
592603
name: this.name,
@@ -597,6 +608,7 @@ export class GraphQLScalarType {
597608
extensions: this.extensions,
598609
astNode: this.astNode,
599610
extensionASTNodes: this.extensionASTNodes,
611+
specifiedBy: this.specifiedBy,
600612
};
601613
}
602614
@@ -628,6 +640,7 @@ export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
628640
extensions?: ?ReadOnlyObjMapLike<mixed>,
629641
astNode?: ?ScalarTypeDefinitionNode,
630642
extensionASTNodes?: ?$ReadOnlyArray<ScalarTypeExtensionNode>,
643+
specifiedBy?: ?string,
631644
|};
632645
633646
/**

src/type/directives.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export const GraphQLIncludeDirective: GraphQLDirective;
5454
*/
5555
export const GraphQLSkipDirective: GraphQLDirective;
5656

57+
/**
58+
* Used to provide a URL for specifying the behavior of custom scalar definitions.
59+
*/
60+
export const GraphQLSpecifiedDirective: GraphQLDirective;
61+
5762
/**
5863
* Constant string used for default reason for a deprecation.
5964
*/

src/type/directives.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,21 @@ export const GraphQLSkipDirective = new GraphQLDirective({
167167
},
168168
});
169169

170+
/**
171+
* Used to provide a URL for specifying the behaviour of custom scalar definitions.
172+
*/
173+
export const GraphQLSpecifiedDirective = new GraphQLDirective({
174+
name: 'specified',
175+
description: 'Exposes a URL that specifies the behaviour of this scalar.',
176+
locations: [DirectiveLocation.SCALAR],
177+
args: {
178+
by: {
179+
type: GraphQLNonNull(GraphQLString),
180+
description: 'The URL that specifies the behaviour of this scalar.',
181+
},
182+
},
183+
});
184+
170185
/**
171186
* Constant string used for default reason for a deprecation.
172187
*/
@@ -195,6 +210,7 @@ export const GraphQLDeprecatedDirective = new GraphQLDirective({
195210
export const specifiedDirectives = Object.freeze([
196211
GraphQLIncludeDirective,
197212
GraphQLSkipDirective,
213+
GraphQLSpecifiedDirective,
198214
GraphQLDeprecatedDirective,
199215
]);
200216

src/type/introspection.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export const __DirectiveLocation = new GraphQLEnumType({
184184
export const __Type = new GraphQLObjectType({
185185
name: '__Type',
186186
description:
187-
'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.',
187+
'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.',
188188
fields: () =>
189189
({
190190
kind: {
@@ -221,6 +221,11 @@ export const __Type = new GraphQLObjectType({
221221
resolve: obj =>
222222
obj.description !== undefined ? obj.description : undefined,
223223
},
224+
specifiedBy: {
225+
type: GraphQLString,
226+
resolve: obj =>
227+
obj.specifiedBy !== undefined ? obj.specifiedBy : undefined,
228+
},
224229
fields: {
225230
type: GraphQLList(GraphQLNonNull(__Field)),
226231
args: {

0 commit comments

Comments
 (0)