Skip to content

Commit 528f8fb

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`.
1 parent fa44c33 commit 528f8fb

17 files changed

+104
-20
lines changed

src/language/__tests__/schema-kitchen-sink.graphql

Lines changed: 2 additions & 0 deletions
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 {

src/language/__tests__/schema-parser-test.js

Lines changed: 1 addition & 0 deletions
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
}

src/language/__tests__/schema-printer-test.js

Lines changed: 2 additions & 0 deletions
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 {

src/language/ast.js

Lines changed: 1 addition & 0 deletions
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

src/language/parser.js

Lines changed: 4 additions & 1 deletion
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
};

src/language/printer.js

Lines changed: 2 additions & 2 deletions
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([

src/language/visitor.js

Lines changed: 1 addition & 1 deletion
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' ],

src/type/__tests__/introspection-test.js

Lines changed: 2 additions & 1 deletion
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
{

src/type/definition.js

Lines changed: 23 additions & 5 deletions
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

src/type/introspection.js

Lines changed: 2 additions & 1 deletion
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,

0 commit comments

Comments
 (0)