Skip to content

Commit a0cf9b2

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 3789a87 commit a0cf9b2

17 files changed

+122
-23
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ scalar CustomScalar
7575

7676
scalar AnnotatedScalar @onScalar
7777

78+
scalar StringEncodedCustomScalar as String
79+
7880
extend scalar CustomScalar @onScalar
7981

8082
enum Site {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ type Hello {
754754
kind: 'ScalarTypeDefinition',
755755
name: nameNode('Hello', { start: 7, end: 12 }),
756756
description: undefined,
757+
type: undefined,
757758
directives: [],
758759
loc: { start: 0, end: 12 },
759760
},

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ describe('Printer: SDL document', () => {
119119
120120
scalar AnnotatedScalar @onScalar
121121
122+
scalar StringEncodedCustomScalar as String
123+
122124
extend scalar CustomScalar @onScalar
123125
124126
enum Site {

src/language/ast.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ export type ScalarTypeDefinitionNode = {
423423
+loc?: Location,
424424
+description?: StringValueNode,
425425
+name: NameNode,
426+
+type?: NamedTypeNode,
426427
+directives?: $ReadOnlyArray<DirectiveNode>,
427428
};
428429

src/language/parser.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -864,18 +864,26 @@ function parseOperationTypeDefinition(
864864
}
865865

866866
/**
867-
* ScalarTypeDefinition : Description? scalar Name Directives[Const]?
867+
* ScalarTypeDefinition :
868+
* - Description? scalar Name ScalarOfType? Directives[Const]?
869+
*
870+
* ScalarOfType : as NamedType
868871
*/
869872
function parseScalarTypeDefinition(lexer: Lexer<*>): ScalarTypeDefinitionNode {
870873
const start = lexer.token;
871874
const description = parseDescription(lexer);
872875
expectKeyword(lexer, 'scalar');
873876
const name = parseName(lexer);
877+
let type;
878+
if (skipKeyword(lexer, 'as')) {
879+
type = parseNamedType(lexer);
880+
}
874881
const directives = parseDirectives(lexer, true);
875882
return {
876883
kind: Kind.SCALAR_TYPE_DEFINITION,
877884
description,
878885
name,
886+
type,
879887
directives,
880888
loc: loc(lexer, start),
881889
};
@@ -1512,10 +1520,24 @@ function expect(lexer: Lexer<*>, kind: TokenKindEnum): Token {
15121520
}
15131521

15141522
/**
1515-
* If the next token is a keyword with the given value, return that token after
1523+
* If the next token is a keyword with the given value, return true after
15161524
* advancing the lexer. Otherwise, do not change the parser state and return
15171525
* false.
15181526
*/
1527+
function skipKeyword(lexer: Lexer<*>, value: string): boolean {
1528+
const token = lexer.token;
1529+
const match = token.kind === TokenKind.NAME && token.value === value;
1530+
if (match) {
1531+
lexer.advance();
1532+
}
1533+
return match;
1534+
}
1535+
1536+
/**
1537+
* If the next token is a keyword with the given value, return that token after
1538+
* advancing the lexer. Otherwise, do not change the parser state and throw
1539+
* an error.
1540+
*/
15191541
function expectKeyword(lexer: Lexer<*>, value: string): Token {
15201542
const token = lexer.token;
15211543
if (token.kind === TokenKind.NAME && token.value === value) {

src/language/printer.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ const printDocASTReducer = {
113113

114114
OperationTypeDefinition: ({ operation, type }) => operation + ': ' + type,
115115

116-
ScalarTypeDefinition: addDescription(({ name, directives }) =>
117-
join(['scalar', name, join(directives, ' ')], ' '),
116+
ScalarTypeDefinition: addDescription(({ name, type, directives }) =>
117+
join(['scalar', name, wrap('as ', type), join(directives, ' ')], ' '),
118118
),
119119

120120
ObjectTypeDefinition: addDescription(

src/language/visitor.js

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

105-
ScalarTypeDefinition: ['description', 'name', 'directives'],
105+
ScalarTypeDefinition: ['description', 'name', 'type', 'directives'],
106106
ObjectTypeDefinition: [
107107
'description',
108108
'name',

src/type/__tests__/introspection-test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1355,7 +1355,9 @@ describe('Introspection', () => {
13551355
'An enum describing what kind of type a given `__Type` is.',
13561356
enumValues: [
13571357
{
1358-
description: 'Indicates this type is a scalar.',
1358+
description:
1359+
'Indicates this type is a scalar. ' +
1360+
'`ofType` may represent how this scalar is serialized.',
13591361
name: 'SCALAR',
13601362
},
13611363
{

src/type/definition.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,17 +538,26 @@ export class GraphQLScalarType {
538538
serialize: GraphQLScalarSerializer<*>;
539539
parseValue: GraphQLScalarValueParser<*>;
540540
parseLiteral: GraphQLScalarLiteralParser<*>;
541+
ofType: ?GraphQLScalarType;
541542
astNode: ?ScalarTypeDefinitionNode;
542543
extensionASTNodes: ?$ReadOnlyArray<ScalarTypeExtensionNode>;
543544

544545
constructor(config: GraphQLScalarTypeConfig<*, *>): void {
545546
this.name = config.name;
546547
this.description = config.description;
547548
this.serialize = config.serialize;
548-
this.parseValue = config.parseValue || (value => value);
549-
this.parseLiteral = config.parseLiteral || valueFromASTUntyped;
549+
this.ofType = config.ofType || null;
550+
this.parseValue =
551+
config.parseValue ||
552+
(this.ofType && this.ofType.parseValue) ||
553+
(value => value);
554+
this.parseLiteral =
555+
config.parseLiteral ||
556+
(this.ofType && this.ofType.parseLiteral) ||
557+
valueFromASTUntyped;
550558
this.astNode = config.astNode;
551559
this.extensionASTNodes = config.extensionASTNodes;
560+
552561
invariant(typeof config.name === 'string', 'Must provide name.');
553562
invariant(
554563
typeof config.serialize === 'function',
@@ -591,6 +600,7 @@ export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
591600
parseValue?: GraphQLScalarValueParser<TInternal>,
592601
// Parses an externally provided literal value to use as an input.
593602
parseLiteral?: GraphQLScalarLiteralParser<TInternal>,
603+
ofType?: ?GraphQLScalarType,
594604
astNode?: ?ScalarTypeDefinitionNode,
595605
extensionASTNodes?: ?$ReadOnlyArray<ScalarTypeExtensionNode>,
596606
|};

src/type/introspection.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ export const __Type = new GraphQLObjectType({
192192
'The fundamental unit of any GraphQL Schema is the type. There are ' +
193193
'many kinds of types in GraphQL as represented by the `__TypeKind` enum.' +
194194
'\n\nDepending on the kind of a type, certain fields describe ' +
195-
'information about that type. Scalar types provide no information ' +
196-
'beyond a name and description, while Enum types provide their values. ' +
195+
'information about that type. Scalar types provide a name, description ' +
196+
'and how they serialize, while Enum types provide their possible values. ' +
197197
'Object and Interface types provide the fields they describe. Abstract ' +
198198
'types, Union and Interface, provide the Object types possible ' +
199199
'at runtime. List and NonNull types compose other types.',
@@ -399,7 +399,9 @@ export const __TypeKind = new GraphQLEnumType({
399399
values: {
400400
SCALAR: {
401401
value: TypeKind.SCALAR,
402-
description: 'Indicates this type is a scalar.',
402+
description:
403+
'Indicates this type is a scalar. ' +
404+
'`ofType` may represent how this scalar is serialized.',
403405
},
404406
OBJECT: {
405407
value: TypeKind.OBJECT,

0 commit comments

Comments
 (0)