Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Define custom scalars in terms of built-in scalars. #1552

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/language/__tests__/schema-kitchen-sink.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ scalar CustomScalar

scalar AnnotatedScalar @onScalar

scalar StringEncodedCustomScalar as String

extend scalar CustomScalar @onScalar

enum Site {
Expand Down
1 change: 1 addition & 0 deletions src/language/__tests__/schema-parser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,7 @@ type Hello {
kind: 'ScalarTypeDefinition',
name: nameNode('Hello', { start: 7, end: 12 }),
description: undefined,
type: undefined,
directives: [],
loc: { start: 0, end: 12 },
},
Expand Down
2 changes: 2 additions & 0 deletions src/language/__tests__/schema-printer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ describe('Printer: SDL document', () => {

scalar AnnotatedScalar @onScalar

scalar StringEncodedCustomScalar as String

extend scalar CustomScalar @onScalar

enum Site {
Expand Down
1 change: 1 addition & 0 deletions src/language/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ export type ScalarTypeDefinitionNode = {
+loc?: Location,
+description?: StringValueNode,
+name: NameNode,
+type?: NamedTypeNode,
+directives?: $ReadOnlyArray<DirectiveNode>,
};

Expand Down
10 changes: 9 additions & 1 deletion src/language/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -838,18 +838,26 @@ function parseOperationTypeDefinition(
}

/**
* ScalarTypeDefinition : Description? scalar Name Directives[Const]?
* ScalarTypeDefinition :
* - Description? scalar Name ScalarOfType? Directives[Const]?
*
* ScalarOfType : as NamedType
*/
function parseScalarTypeDefinition(lexer: Lexer<*>): ScalarTypeDefinitionNode {
const start = lexer.token;
const description = parseDescription(lexer);
expectKeyword(lexer, 'scalar');
const name = parseName(lexer);
let type;
if (skipKeyword(lexer, 'as')) {
type = parseNamedType(lexer);
}
const directives = parseDirectives(lexer, true);
return {
kind: Kind.SCALAR_TYPE_DEFINITION,
description,
name,
type,
directives,
loc: loc(lexer, start),
};
Expand Down
4 changes: 2 additions & 2 deletions src/language/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ const printDocASTReducer = {

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

ScalarTypeDefinition: addDescription(({ name, directives }) =>
join(['scalar', name, join(directives, ' ')], ' '),
ScalarTypeDefinition: addDescription(({ name, type, directives }) =>
join(['scalar', name, wrap('as ', type), join(directives, ' ')], ' '),
),

ObjectTypeDefinition: addDescription(
Expand Down
2 changes: 1 addition & 1 deletion src/language/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const QueryDocumentKeys = {
SchemaDefinition: ['directives', 'operationTypes'],
OperationTypeDefinition: ['type'],

ScalarTypeDefinition: ['description', 'name', 'directives'],
ScalarTypeDefinition: ['description', 'name', 'type', 'directives'],
ObjectTypeDefinition: [
'description',
'name',
Expand Down
4 changes: 3 additions & 1 deletion src/type/__tests__/introspection-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1357,7 +1357,9 @@ describe('Introspection', () => {
'An enum describing what kind of type a given `__Type` is.',
enumValues: [
{
description: 'Indicates this type is a scalar.',
description:
'Indicates this type is a scalar. ' +
'`ofType` may represent how this scalar is serialized.',
name: 'SCALAR',
},
{
Expand Down
14 changes: 12 additions & 2 deletions src/type/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -538,17 +538,26 @@ export class GraphQLScalarType {
serialize: GraphQLScalarSerializer<*>;
parseValue: GraphQLScalarValueParser<*>;
parseLiteral: GraphQLScalarLiteralParser<*>;
ofType: ?GraphQLScalarType;
astNode: ?ScalarTypeDefinitionNode;
extensionASTNodes: ?$ReadOnlyArray<ScalarTypeExtensionNode>;

constructor(config: GraphQLScalarTypeConfig<*, *>): void {
this.name = config.name;
this.description = config.description;
this.serialize = config.serialize;
this.parseValue = config.parseValue || (value => value);
this.parseLiteral = config.parseLiteral || valueFromASTUntyped;
this.ofType = config.ofType || null;
this.parseValue =
config.parseValue ||
(this.ofType && this.ofType.parseValue) ||
(value => value);
this.parseLiteral =
config.parseLiteral ||
(this.ofType && this.ofType.parseLiteral) ||
valueFromASTUntyped;
this.astNode = config.astNode;
this.extensionASTNodes = config.extensionASTNodes;

invariant(typeof config.name === 'string', 'Must provide name.');
invariant(
typeof config.serialize === 'function',
Expand Down Expand Up @@ -591,6 +600,7 @@ export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
parseValue?: GraphQLScalarValueParser<TInternal>,
// Parses an externally provided literal value to use as an input.
parseLiteral?: GraphQLScalarLiteralParser<TInternal>,
ofType?: ?GraphQLScalarType,
astNode?: ?ScalarTypeDefinitionNode,
extensionASTNodes?: ?$ReadOnlyArray<ScalarTypeExtensionNode>,
|};
Expand Down
8 changes: 5 additions & 3 deletions src/type/introspection.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ export const __Type = new GraphQLObjectType({
'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. ' +
'information about that type. Scalar types provide a name, description ' +
'and how they serialize, while Enum types provide their possible 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.',
Expand Down Expand Up @@ -399,7 +399,9 @@ export const __TypeKind = new GraphQLEnumType({
values: {
SCALAR: {
value: TypeKind.SCALAR,
description: 'Indicates this type is a scalar.',
description:
'Indicates this type is a scalar. ' +
'`ofType` may represent how this scalar is serialized.',
},
OBJECT: {
value: TypeKind.OBJECT,
Expand Down
22 changes: 21 additions & 1 deletion src/type/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import {
isScalarType,
isObjectType,
isInterfaceType,
isUnionType,
Expand All @@ -19,6 +20,7 @@ import {
isRequiredArgument,
} from './definition';
import type {
GraphQLScalarType,
GraphQLObjectType,
GraphQLInterfaceType,
GraphQLUnionType,
Expand All @@ -28,6 +30,7 @@ import type {
import { isDirective } from './directives';
import type { GraphQLDirective } from './directives';
import { isIntrospectionType } from './introspection';
import { isSpecifiedScalarType } from './scalars';
import { isSchema } from './schema';
import type { GraphQLSchema } from './schema';
import inspect from '../jsutils/inspect';
Expand Down Expand Up @@ -244,7 +247,10 @@ function validateTypes(context: SchemaValidationContext): void {
validateName(context, type);
}

if (isObjectType(type)) {
if (isScalarType(type)) {
// Ensure Scalars can serialize as expected.
validateScalarSerialization(context, type);
} else if (isObjectType(type)) {
// Ensure fields are valid
validateFields(context, type);

Expand All @@ -266,6 +272,20 @@ function validateTypes(context: SchemaValidationContext): void {
}
}

function validateScalarSerialization(
context: SchemaValidationContext,
scalarType: GraphQLScalarType,
): void {
if (scalarType.ofType && !isSpecifiedScalarType(scalarType.ofType)) {
context.reportError(
`Scalar type ${scalarType.name} may only be described in terms of a ` +
`spec-defined scalar type. However ${String(scalarType.ofType)} is ` +
'not a built-in scalar type.',
scalarType.astNode && scalarType.astNode.type,
);
}
}

function validateFields(
context: SchemaValidationContext,
type: GraphQLObjectType | GraphQLInterfaceType,
Expand Down
35 changes: 25 additions & 10 deletions src/utilities/__tests__/schemaPrinter-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,17 @@ describe('Type System Printer', () => {
});

it('Custom Scalar', () => {
const EvenType = new GraphQLScalarType({
name: 'Even',
ofType: GraphQLInt,
serialize(value) {
return value % 2 === 1 ? value : null;
},
});

const OddType = new GraphQLScalarType({
name: 'Odd',
// No ofType in this test case.
serialize(value) {
return value % 2 === 1 ? value : null;
},
Expand All @@ -473,16 +482,20 @@ describe('Type System Printer', () => {
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
even: { type: EvenType },
odd: { type: OddType },
},
});

const Schema = new GraphQLSchema({ query: Query });
const output = printForTest(Schema);
expect(output).to.equal(dedent`
scalar Even as Int

scalar Odd

type Query {
even: Even
odd: Odd
}
`);
Expand Down Expand Up @@ -787,10 +800,10 @@ describe('Type System Printer', () => {
types in GraphQL as represented by the \`__TypeKind\` enum.

Depending 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.
type. Scalar types provide a name, description and how they serialize, while
Enum types provide their possible 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.
"""
type __Type {
kind: __TypeKind!
Expand All @@ -806,7 +819,9 @@ describe('Type System Printer', () => {

"""An enum describing what kind of type a given \`__Type\` is."""
enum __TypeKind {
"""Indicates this type is a scalar."""
"""
Indicates this type is a scalar. \`ofType\` may represent how this scalar is serialized.
"""
SCALAR

"""
Expand Down Expand Up @@ -1003,10 +1018,10 @@ describe('Type System Printer', () => {
# types in GraphQL as represented by the \`__TypeKind\` enum.
#
# Depending 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.
# type. Scalar types provide a name, description and how they serialize, while
# Enum types provide their possible 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.
type __Type {
kind: __TypeKind!
name: String
Expand All @@ -1021,7 +1036,7 @@ describe('Type System Printer', () => {

# An enum describing what kind of type a given \`__Type\` is.
enum __TypeKind {
# Indicates this type is a scalar.
# Indicates this type is a scalar. \`ofType\` may represent how this scalar is serialized.
SCALAR

# Indicates this type is an object. \`fields\` and \`interfaces\` are valid fields.
Expand Down
4 changes: 4 additions & 0 deletions src/utilities/buildASTSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,10 @@ export class ASTDefinitionBuilder {
return new GraphQLScalarType({
name: def.name.value,
description: getDescription(def, this._options),
// Note: While this could make assertions to get the correctly typed
// values below, that would throw immediately while type system
// validation with validateSchema() will produce more actionable results.
ofType: def.type && (this.buildType(def.type): any),
astNode: def,
serialize: value => value,
});
Expand Down
4 changes: 4 additions & 0 deletions src/utilities/buildClientSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,13 @@ export function buildClientSchema(
function buildScalarDef(
scalarIntrospection: IntrospectionScalarType,
): GraphQLScalarType {
const ofType = scalarIntrospection.ofType
? (getType(scalarIntrospection.ofType): any)
: undefined;
return new GraphQLScalarType({
name: scalarIntrospection.name,
description: scalarIntrospection.description,
ofType,
serialize: value => value,
});
}
Expand Down
12 changes: 12 additions & 0 deletions src/utilities/findBreakingChanges.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,18 @@ export function findTypesThatChangedKind(
`${typeName} changed from ` +
`${typeKindName(oldType)} to ${typeKindName(newType)}.`,
});
} else if (isScalarType(oldType) && isScalarType(newType)) {
const oldOfType = oldType.ofType;
const newOfType = newType.ofType;
if (oldOfType && newOfType && oldOfType !== newOfType) {
breakingChanges.push({
type: BreakingChangeType.TYPE_CHANGED_KIND,
description:
`${typeName} changed from ` +
`${typeKindName(oldType)} serialized as ${oldOfType.name} ` +
`to ${typeKindName(newType)} serialized as ${newOfType.name}.`,
});
}
}
}
return breakingChanges;
Expand Down
1 change: 1 addition & 0 deletions src/utilities/introspectionQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export type IntrospectionScalarType = {
+kind: 'SCALAR',
+name: string,
+description?: ?string,
+ofType?: ?IntrospectionNamedTypeRef<IntrospectionScalarType>,
};

export type IntrospectionObjectType = {
Expand Down
3 changes: 2 additions & 1 deletion src/utilities/schemaPrinter.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ export function printType(type: GraphQLNamedType, options?: Options): string {
}

function printScalar(type: GraphQLScalarType, options): string {
return printDescription(options, type) + `scalar ${type.name}`;
const ofType = type.ofType ? ` as ${type.ofType.name}` : '';
return printDescription(options, type) + `scalar ${type.name}${ofType}`;
}

function printObject(type: GraphQLObjectType, options): string {
Expand Down