Skip to content

Commit 0a2aec0

Browse files
committed
RFC: Define custom scalars in terms of built-in scalars.
This proposes an additive change which allows custom scalars to be defined in terms of the built-in scalars. The motivation is for client-side code generators to understand how to map between the GraphQL type system and a native type system. As an example, a `URL` custom type may be defined in terms of the built-in scalar `String`. It could define additional serialization and parsing logic, however client tools can know to treat `URL` values as `String`. Presently, we do this by defining these mappings manually on the client, which is difficult to scale, or by giving up and making no assumptions of how the custom types serialize. Another real use case of giving client tools this information is GraphiQL: this change will allow GraphiQL to show more useful errors when a literal of an incorrect kind is provided to a custom scalar. Currently GraphiQL simply accepts all values. To accomplish this, this proposes adding the following: * A new property when defining `GraphQLScalarType` (`ofType`) which asserts that only built-in scalar types are provided. * A second type coercion to guarantee to a client that the serialized values match the `ofType`. * Delegating the `parseLiteral` and `parseValue` functions to those in `ofType` (this enables downstream validation / GraphiQL features) * Exposing `ofType` in the introspection system, and consuming that introspection in `buildClientSchema`. * Adding optional syntax to the SDL, and consuming that in `buildASTSchema` and `extendSchema` as well as in `schemaPrinter`. * Adding a case to `findBreakingChanges` which looks for a scalar's ofType changing.
1 parent fa44c33 commit 0a2aec0

17 files changed

+106
-20
lines changed

Diff for: src/language/__tests__/schema-kitchen-sink.graphql

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ union AnnotatedUnionTwo @onUnion = | A | B
4141

4242
scalar CustomScalar
4343

44+
scalar StringEncodedCustomScalar = String
45+
4446
scalar AnnotatedScalar @onScalar
4547

4648
enum Site {

Diff for: src/language/__tests__/schema-parser-test.js

+1
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ type Hello {
530530
{
531531
kind: 'ScalarTypeDefinition',
532532
name: nameNode('Hello', { start: 7, end: 12 }),
533+
type: null,
533534
directives: [],
534535
loc: { start: 0, end: 12 },
535536
}

Diff for: src/language/__tests__/schema-printer-test.js

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ union AnnotatedUnionTwo @onUnion = A | B
8787
8888
scalar CustomScalar
8989
90+
scalar StringEncodedCustomScalar = String
91+
9092
scalar AnnotatedScalar @onScalar
9193
9294
enum Site {

Diff for: src/language/ast.js

+1
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ export type ScalarTypeDefinitionNode = {
397397
kind: 'ScalarTypeDefinition';
398398
loc?: Location;
399399
name: NameNode;
400+
type?: ?NamedTypeNode;
400401
directives?: ?Array<DirectiveNode>;
401402
};
402403

Diff for: src/language/parser.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -784,16 +784,19 @@ function parseOperationTypeDefinition(
784784
}
785785

786786
/**
787-
* ScalarTypeDefinition : scalar Name Directives?
787+
* ScalarTypeDefinition : scalar Name ScalarOfType? Directives?
788+
* ScalarOfType : = NamedType
788789
*/
789790
function parseScalarTypeDefinition(lexer: Lexer<*>): ScalarTypeDefinitionNode {
790791
const start = lexer.token;
791792
expectKeyword(lexer, 'scalar');
792793
const name = parseName(lexer);
794+
const type = skip(lexer, TokenKind.EQUALS) ? parseNamedType(lexer) : null;
793795
const directives = parseDirectives(lexer);
794796
return {
795797
kind: SCALAR_TYPE_DEFINITION,
796798
name,
799+
type,
797800
directives,
798801
loc: loc(lexer, start),
799802
};

Diff for: src/language/printer.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ const printDocASTReducer = {
105105
OperationTypeDefinition: ({ operation, type }) =>
106106
operation + ': ' + type,
107107

108-
ScalarTypeDefinition: ({ name, directives }) =>
109-
join([ 'scalar', name, join(directives, ' ') ], ' '),
108+
ScalarTypeDefinition: ({ name, type, directives }) =>
109+
join([ 'scalar', name, wrap('= ', type), join(directives, ' ') ], ' '),
110110

111111
ObjectTypeDefinition: ({ name, interfaces, directives, fields }) =>
112112
join([

Diff for: src/language/visitor.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const QueryDocumentKeys = {
4242
SchemaDefinition: [ 'directives', 'operationTypes' ],
4343
OperationTypeDefinition: [ 'type' ],
4444

45-
ScalarTypeDefinition: [ 'name', 'directives' ],
45+
ScalarTypeDefinition: [ 'name', 'type', 'directives' ],
4646
ObjectTypeDefinition: [ 'name', 'interfaces', 'directives', 'fields' ],
4747
FieldDefinition: [ 'name', 'arguments', 'type', 'directives' ],
4848
InputValueDefinition: [ 'name', 'type', 'defaultValue', 'directives' ],

Diff for: src/type/__tests__/introspection-test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1313,7 +1313,8 @@ describe('Introspection', () => {
13131313
'An enum describing what kind of type a given `__Type` is.',
13141314
enumValues: [
13151315
{
1316-
description: 'Indicates this type is a scalar.',
1316+
description: 'Indicates this type is a scalar. ' +
1317+
'`ofType` is a valid field.',
13171318
name: 'SCALAR'
13181319
},
13191320
{

Diff for: src/type/definition.js

+23-5
Original file line numberDiff line numberDiff line change
@@ -294,13 +294,27 @@ function resolveThunk<T>(thunk: Thunk<T>): T {
294294
export class GraphQLScalarType {
295295
name: string;
296296
description: ?string;
297+
ofType: ?GraphQLScalarType;
297298

298299
_scalarConfig: GraphQLScalarTypeConfig<*, *>;
299300

300301
constructor(config: GraphQLScalarTypeConfig<*, *>): void {
301302
assertValidName(config.name);
302303
this.name = config.name;
303304
this.description = config.description;
305+
this.ofType = config.ofType || null;
306+
if (this.ofType) {
307+
const ofTypeName = this.ofType.name;
308+
invariant(
309+
ofTypeName === 'String' ||
310+
ofTypeName === 'Int' ||
311+
ofTypeName === 'Float' ||
312+
ofTypeName === 'Boolean' ||
313+
ofTypeName === 'ID',
314+
`${this.name} may only be described in terms of a built-in scalar ` +
315+
`type. However ${ofTypeName} is not a built-in scalar type.`
316+
);
317+
}
304318
invariant(
305319
typeof config.serialize === 'function',
306320
`${this.name} must provide "serialize" function. If this custom Scalar ` +
@@ -321,7 +335,8 @@ export class GraphQLScalarType {
321335
// Serializes an internal value to include in a response.
322336
serialize(value: mixed): mixed {
323337
const serializer = this._scalarConfig.serialize;
324-
return serializer(value);
338+
const serialized = serializer(value);
339+
return this.ofType ? this.ofType.serialize(serialized) : serialized;
325340
}
326341

327342
// Determines if an internal value is valid for this type.
@@ -332,7 +347,8 @@ export class GraphQLScalarType {
332347

333348
// Parses an externally provided value to use as an input.
334349
parseValue(value: mixed): mixed {
335-
const parser = this._scalarConfig.parseValue;
350+
const parser = this._scalarConfig.parseValue ||
351+
this.ofType && this.ofType.parseValue;
336352
return parser && !isNullish(value) ? parser(value) : undefined;
337353
}
338354

@@ -344,7 +360,8 @@ export class GraphQLScalarType {
344360

345361
// Parses an externally provided literal value to use as an input.
346362
parseLiteral(valueNode: ValueNode): mixed {
347-
const parser = this._scalarConfig.parseLiteral;
363+
const parser = this._scalarConfig.parseLiteral ||
364+
this.ofType && this.ofType.parseLiteral;
348365
return parser ? parser(valueNode) : undefined;
349366
}
350367

@@ -364,9 +381,10 @@ GraphQLScalarType.prototype.toJSON =
364381
export type GraphQLScalarTypeConfig<TInternal, TExternal> = {
365382
name: string;
366383
description?: ?string;
384+
ofType?: ?GraphQLScalarType;
367385
serialize: (value: mixed) => ?TExternal;
368-
parseValue?: (value: mixed) => ?TInternal;
369-
parseLiteral?: (valueNode: ValueNode) => ?TInternal;
386+
parseValue?: ?(value: mixed) => ?TInternal;
387+
parseLiteral?: ?(valueNode: ValueNode) => ?TInternal;
370388
};
371389

372390

Diff for: src/type/introspection.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,8 @@ export const __TypeKind = new GraphQLEnumType({
381381
values: {
382382
SCALAR: {
383383
value: TypeKind.SCALAR,
384-
description: 'Indicates this type is a scalar.'
384+
description: 'Indicates this type is a scalar. ' +
385+
'`ofType` is a valid field.'
385386
},
386387
OBJECT: {
387388
value: TypeKind.OBJECT,

Diff for: src/utilities/__tests__/schemaPrinter-test.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -540,8 +540,17 @@ type Root {
540540
});
541541

542542
it('Custom Scalar', () => {
543+
const EvenType = new GraphQLScalarType({
544+
name: 'Even',
545+
ofType: GraphQLInt,
546+
serialize(value) {
547+
return value % 2 === 1 ? value : null;
548+
}
549+
});
550+
543551
const OddType = new GraphQLScalarType({
544552
name: 'Odd',
553+
// No ofType in this test case.
545554
serialize(value) {
546555
return value % 2 === 1 ? value : null;
547556
}
@@ -550,6 +559,7 @@ type Root {
550559
const Root = new GraphQLObjectType({
551560
name: 'Root',
552561
fields: {
562+
even: { type: EvenType },
553563
odd: { type: OddType },
554564
},
555565
});
@@ -561,9 +571,12 @@ schema {
561571
query: Root
562572
}
563573
574+
scalar Even = Int
575+
564576
scalar Odd
565577
566578
type Root {
579+
even: Even
567580
odd: Odd
568581
}
569582
`
@@ -788,7 +801,7 @@ type __Type {
788801
789802
# An enum describing what kind of type a given \`__Type\` is.
790803
enum __TypeKind {
791-
# Indicates this type is a scalar.
804+
# Indicates this type is a scalar. \`ofType\` is a valid field.
792805
SCALAR
793806
794807
# Indicates this type is an object. \`fields\` and \`interfaces\` are valid fields.

Diff for: src/utilities/buildASTSchema.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,12 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema {
313313
return type;
314314
}
315315

316+
function produceScalarType(typeNode: TypeNode): GraphQLScalarType {
317+
const type = produceType(typeNode);
318+
invariant(type instanceof GraphQLScalarType, 'Expected Scalar type.');
319+
return type;
320+
}
321+
316322
function typeDefNamed(typeName: string): GraphQLNamedType {
317323
if (innerTypeMap[typeName]) {
318324
return innerTypeMap[typeName];
@@ -434,16 +440,18 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema {
434440
}
435441

436442
function makeScalarDef(def: ScalarTypeDefinitionNode) {
443+
const ofType = def.type && produceScalarType(def.type);
437444
return new GraphQLScalarType({
438445
name: def.name.value,
439446
description: getDescription(def),
440-
serialize: () => null,
447+
ofType,
448+
serialize: id => id,
441449
// Note: validation calls the parse functions to determine if a
442450
// literal value is correct. Returning null would cause use of custom
443451
// scalars to always fail validation. Returning false causes them to
444452
// always pass validation.
445-
parseValue: () => false,
446-
parseLiteral: () => false,
453+
parseValue: ofType ? null : () => false,
454+
parseLiteral: ofType ? null : () => false,
447455
});
448456
}
449457

Diff for: src/utilities/buildClientSchema.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,14 @@ export function buildClientSchema(
194194
return type;
195195
}
196196

197+
function getScalarType(typeRef: IntrospectionTypeRef): GraphQLScalarType {
198+
const type = getType(typeRef);
199+
invariant(
200+
type instanceof GraphQLScalarType,
201+
'Introspection must provide scalar type for custom scalars.'
202+
);
203+
return type;
204+
}
197205

198206
// Given a type's introspection result, construct the correct
199207
// GraphQLType instance.
@@ -223,16 +231,20 @@ export function buildClientSchema(
223231
function buildScalarDef(
224232
scalarIntrospection: IntrospectionScalarType
225233
): GraphQLScalarType {
234+
const ofType = scalarIntrospection.ofType ?
235+
getScalarType(scalarIntrospection.ofType) :
236+
undefined;
226237
return new GraphQLScalarType({
227238
name: scalarIntrospection.name,
228239
description: scalarIntrospection.description,
240+
ofType,
229241
serialize: id => id,
230242
// Note: validation calls the parse functions to determine if a
231243
// literal value is correct. Returning null would cause use of custom
232244
// scalars to always fail validation. Returning false causes them to
233245
// always pass validation.
234-
parseValue: () => false,
235-
parseLiteral: () => false,
246+
parseValue: ofType ? null : () => false,
247+
parseLiteral: ofType ? null : () => false,
236248
});
237249
}
238250

Diff for: src/utilities/extendSchema.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,12 @@ export function extendSchema(
285285
return type;
286286
}
287287

288+
function getScalarTypeFromAST(node: NamedTypeNode): GraphQLScalarType {
289+
const type = getTypeFromAST(node);
290+
invariant(type instanceof GraphQLScalarType, 'Must be Scalar type.');
291+
return type;
292+
}
293+
288294
function getInputTypeFromAST(node: NamedTypeNode): GraphQLInputType {
289295
return assertInputType(getTypeFromAST(node));
290296
}
@@ -478,16 +484,18 @@ export function extendSchema(
478484
}
479485

480486
function buildScalarType(typeNode: ScalarTypeDefinitionNode) {
487+
const ofType = typeNode.type && getScalarTypeFromAST(typeNode.type);
481488
return new GraphQLScalarType({
482489
name: typeNode.name.value,
483490
description: getDescription(typeNode),
491+
ofType,
484492
serialize: id => id,
485493
// Note: validation calls the parse functions to determine if a
486494
// literal value is correct. Returning null would cause use of custom
487495
// scalars to always fail validation. Returning false causes them to
488496
// always pass validation.
489-
parseValue: () => false,
490-
parseLiteral: () => false,
497+
parseValue: ofType ? null : () => false,
498+
parseLiteral: ofType ? null : () => false,
491499
});
492500
}
493501

Diff for: src/utilities/findBreakingChanges.js

+14
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,20 @@ export function findTypesThatChangedKind(
135135
description: `${typeName} changed from ` +
136136
`${typeKindName(oldType)} to ${typeKindName(newType)}.`
137137
});
138+
} else if (
139+
oldType instanceof GraphQLScalarType &&
140+
newType instanceof GraphQLScalarType
141+
) {
142+
const oldOfType = oldType.ofType;
143+
const newOfType = newType.ofType;
144+
if (oldOfType && newOfType && oldOfType !== newOfType) {
145+
breakingChanges.push({
146+
type: BreakingChangeType.TYPE_CHANGED_KIND,
147+
description: `${typeName} changed from ` +
148+
`${typeKindName(oldType)} serialized as ${oldOfType.name} ` +
149+
`to ${typeKindName(newType)} serialized as ${newOfType.name}.`
150+
});
151+
}
138152
}
139153
});
140154
return breakingChanges;

Diff for: src/utilities/introspectionQuery.js

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export type IntrospectionScalarType = {
129129
kind: 'SCALAR';
130130
name: string;
131131
description: ?string;
132+
ofType: ?IntrospectionNamedTypeRef;
132133
};
133134

134135
export type IntrospectionObjectType = {

Diff for: src/utilities/schemaPrinter.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,9 @@ export function printType(type: GraphQLType): string {
153153
}
154154

155155
function printScalar(type: GraphQLScalarType): string {
156+
const ofType = type.ofType ? ` = ${type.ofType.name}` : '';
156157
return printDescription(type) +
157-
`scalar ${type.name}`;
158+
`scalar ${type.name}${ofType}`;
158159
}
159160

160161
function printObject(type: GraphQLObjectType): string {

0 commit comments

Comments
 (0)