Skip to content

Commit 3125978

Browse files
leebyronIvanGoncharov
authored andcommitted
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 fd4a69c commit 3125978

16 files changed

+84
-11
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ extend union Feed @onUnion
6565

6666
scalar CustomScalar
6767

68+
scalar StringEncodedCustomScalar = String
69+
6870
scalar AnnotatedScalar @onScalar
6971

7072
extend scalar CustomScalar @onScalar

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,7 @@ type Hello {
712712
{
713713
kind: 'ScalarTypeDefinition',
714714
name: nameNode('Hello', { start: 7, end: 12 }),
715+
type: null,
715716
directives: [],
716717
loc: { start: 0, end: 12 },
717718
},

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ describe('Printer', () => {
111111
112112
scalar AnnotatedScalar @onScalar
113113
114+
scalar StringEncodedCustomScalar = String
115+
116+
scalar AnnotatedScalar @onScalar
117+
114118
extend scalar CustomScalar @onScalar
115119
116120
enum Site {

src/language/ast.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ export type ScalarTypeDefinitionNode = {
448448
+loc?: Location,
449449
+description?: StringValueNode,
450450
+name: NameNode,
451+
+type?: NamedTypeNode;
451452
+directives?: $ReadOnlyArray<DirectiveNode>,
452453
};
453454

src/language/parser.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,18 +890,23 @@ function parseOperationTypeDefinition(
890890
}
891891

892892
/**
893-
* ScalarTypeDefinition : Description? scalar Name Directives[Const]?
893+
* ScalarTypeDefinition :
894+
* - Description? scalar Name ScalarOfType? Directives[Const]?
895+
*
896+
* ScalarOfType : = NamedType
894897
*/
895898
function parseScalarTypeDefinition(lexer: Lexer<*>): ScalarTypeDefinitionNode {
896899
const start = lexer.token;
897900
const description = parseDescription(lexer);
898901
expectKeyword(lexer, 'scalar');
899902
const name = parseName(lexer);
903+
const type = skip(lexer, TokenKind.EQUALS) ? parseNamedType(lexer) : null;
900904
const directives = parseDirectives(lexer, true);
901905
return {
902906
kind: SCALAR_TYPE_DEFINITION,
903907
description,
904908
name,
909+
type,
905910
directives,
906911
loc: loc(lexer, start),
907912
};

src/language/printer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ const printDocASTReducer = {
111111
OperationTypeDefinition: ({ operation, type }) => operation + ': ' + type,
112112

113113
ScalarTypeDefinition: addDescription(({ name, directives }) =>
114-
join(['scalar', name, join(directives, ' ')], ' '),
114+
join(['scalar', name, wrap(' = ', type), join(directives, ' ')], ' '),
115115
),
116116

117117
ObjectTypeDefinition: addDescription(

src/language/visitor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const QueryDocumentKeys = {
101101
SchemaDefinition: ['directives', 'operationTypes'],
102102
OperationTypeDefinition: ['type'],
103103

104-
ScalarTypeDefinition: ['description', 'name', 'directives'],
104+
ScalarTypeDefinition: ['description', 'name', 'type', 'directives'],
105105
ObjectTypeDefinition: [
106106
'description',
107107
'name',

src/type/__tests__/introspection-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,8 +1307,8 @@ describe('Introspection', () => {
13071307
'An enum describing what kind of type a given `__Type` is.',
13081308
enumValues: [
13091309
{
1310-
description: 'Indicates this type is a scalar.',
1311-
name: 'SCALAR',
1310+
description: 'Indicates this type is a scalar. `ofType` is a valid field.',
1311+
name: 'SCALAR'
13121312
},
13131313
{
13141314
description:

src/type/definition.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -449,16 +449,30 @@ function resolveThunk<+T>(thunk: Thunk<T>): T {
449449
export class GraphQLScalarType {
450450
name: string;
451451
description: ?string;
452+
ofType: ?GraphQLScalarType;
452453
astNode: ?ScalarTypeDefinitionNode;
453454

454455
_scalarConfig: GraphQLScalarTypeConfig<*, *>;
455456

456457
constructor(config: GraphQLScalarTypeConfig<*, *>): void {
457458
this.name = config.name;
458459
this.description = config.description;
460+
this.ofType = config.ofType || null;
459461
this.astNode = config.astNode;
460462
this._scalarConfig = config;
461463
invariant(typeof config.name === 'string', 'Must provide name.');
464+
if (this.ofType) {
465+
const ofTypeName = this.ofType.name;
466+
invariant(
467+
ofTypeName === 'String' ||
468+
ofTypeName === 'Int' ||
469+
ofTypeName === 'Float' ||
470+
ofTypeName === 'Boolean' ||
471+
ofTypeName === 'ID',
472+
`${this.name} may only be described in terms of a built-in scalar ` +
473+
`type. However ${ofTypeName} is not a built-in scalar type.`
474+
);
475+
}
462476
invariant(
463477
typeof config.serialize === 'function',
464478
`${this.name} must provide "serialize" function. If this custom Scalar ` +
@@ -478,12 +492,13 @@ export class GraphQLScalarType {
478492
// Serializes an internal value to include in a response.
479493
serialize(value: mixed): mixed {
480494
const serializer = this._scalarConfig.serialize;
481-
return serializer(value);
495+
const serialized = serializer(value);
496+
return this.ofType ? this.ofType.serialize(serialized) : serialized;
482497
}
483498

484499
// Parses an externally provided value to use as an input.
485500
parseValue(value: mixed): mixed {
486-
const parser = this._scalarConfig.parseValue;
501+
const parser = this._scalarConfig.parseValue || (this.ofType && this.ofType.parseValue);
487502
if (isInvalid(value)) {
488503
return undefined;
489504
}
@@ -492,7 +507,7 @@ export class GraphQLScalarType {
492507

493508
// Parses an externally provided literal value to use as an input.
494509
parseLiteral(valueNode: ValueNode, variables: ?ObjMap<mixed>): mixed {
495-
const parser = this._scalarConfig.parseLiteral;
510+
const parser = this._scalarConfig.parseLiteral || (this.ofType && this.ofType.parseLiteral);
496511
return parser
497512
? parser(valueNode, variables)
498513
: valueFromASTUntyped(valueNode, variables);
@@ -513,6 +528,7 @@ GraphQLScalarType.prototype.toJSON = GraphQLScalarType.prototype.inspect =
513528
export type GraphQLScalarTypeConfig<TInternal, TExternal> = {
514529
name: string,
515530
description?: ?string,
531+
ofType?: ?GraphQLScalarType,
516532
astNode?: ?ScalarTypeDefinitionNode,
517533
serialize: (value: mixed) => ?TExternal,
518534
parseValue?: (value: mixed) => ?TInternal,

src/type/introspection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ 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. `ofType` is a valid field.',
385385
},
386386
OBJECT: {
387387
value: TypeKind.OBJECT,

0 commit comments

Comments
 (0)