Skip to content

Rebase of "Schema scalar of type" #1173

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

Closed
Closed
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 @@ -65,6 +65,8 @@ extend union Feed @onUnion

scalar CustomScalar

scalar StringEncodedCustomScalar as String

scalar AnnotatedScalar @onScalar

extend scalar CustomScalar @onScalar
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 @@ -712,6 +712,7 @@ type Hello {
{
kind: 'ScalarTypeDefinition',
name: nameNode('Hello', { start: 7, end: 12 }),
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 @@ -109,6 +109,8 @@ describe('Printer', () => {

scalar CustomScalar

scalar StringEncodedCustomScalar as String

scalar AnnotatedScalar @onScalar

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

Expand Down
21 changes: 19 additions & 2 deletions src/language/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -890,18 +890,21 @@ 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);
const type = skipKeyword(lexer, 'as') ? parseNamedType(lexer) : undefined;
const directives = parseDirectives(lexer, true);
return {
kind: SCALAR_TYPE_DEFINITION,
description,
name,
type,
directives,
loc: loc(lexer, start),
};
Expand Down Expand Up @@ -1503,10 +1506,24 @@ function expect(lexer: Lexer<*>, kind: string): Token {
}

/**
* If the next token is a keyword with the given value, return that token after
* If the next token is a keyword with the given value, return true after
* advancing the lexer. Otherwise, do not change the parser state and return
* false.
*/
function skipKeyword(lexer: Lexer<*>, value: string): boolean {
const token = lexer.token;
const match = token.kind === TokenKind.NAME && token.value === value;
if (match) {
lexer.advance();
}
return match;
}

/**
* If the next token is a keyword with the given value, return that token after
* advancing the lexer. Otherwise, do not change the parser state and throw
* an error.
*/
function expectKeyword(lexer: Lexer<*>, value: string): Token {
const token = lexer.token;
if (token.kind === TokenKind.NAME && token.value === value) {
Expand Down
8 changes: 5 additions & 3 deletions src/language/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,14 @@ const printDocASTReducer = {

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

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

ObjectTypeDefinition: ({
description,
name,
Expand Down
2 changes: 1 addition & 1 deletion src/language/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,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 @@ -1307,7 +1307,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
13 changes: 10 additions & 3 deletions src/type/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,12 +450,14 @@ export class GraphQLScalarType {
name: string;
description: ?string;
astNode: ?ScalarTypeDefinitionNode;
ofType: ?GraphQLScalarType;

_scalarConfig: GraphQLScalarTypeConfig<*, *>;

constructor(config: GraphQLScalarTypeConfig<*, *>): void {
this.name = config.name;
this.description = config.description;
this.ofType = config.ofType || null;
this.astNode = config.astNode;
this._scalarConfig = config;
invariant(typeof config.name === 'string', 'Must provide name.');
Expand All @@ -478,12 +480,14 @@ export class GraphQLScalarType {
// Serializes an internal value to include in a response.
serialize(value: mixed): mixed {
const serializer = this._scalarConfig.serialize;
return serializer(value);
const serialized = serializer(value);
return this.ofType ? this.ofType.serialize(serialized) : serialized;
}

// Parses an externally provided value to use as an input.
parseValue(value: mixed): mixed {
const parser = this._scalarConfig.parseValue;
const parser =
this._scalarConfig.parseValue || (this.ofType && this.ofType.parseValue);
if (isInvalid(value)) {
return undefined;
}
Expand All @@ -492,7 +496,9 @@ export class GraphQLScalarType {

// Parses an externally provided literal value to use as an input.
parseLiteral(valueNode: ValueNode, variables: ?ObjMap<mixed>): mixed {
const parser = this._scalarConfig.parseLiteral;
const parser =
this._scalarConfig.parseLiteral ||
(this.ofType && this.ofType.parseLiteral);
return parser
? parser(valueNode, variables)
: valueFromASTUntyped(valueNode, variables);
Expand All @@ -513,6 +519,7 @@ GraphQLScalarType.prototype.toJSON = GraphQLScalarType.prototype.inspect =
export type GraphQLScalarTypeConfig<TInternal, TExternal> = {
name: string,
description?: ?string,
ofType?: ?GraphQLScalarType,
astNode?: ?ScalarTypeDefinitionNode,
serialize: (value: mixed) => ?TExternal,
parseValue?: (value: mixed) => ?TInternal,
Expand Down
8 changes: 5 additions & 3 deletions src/type/introspection.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,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 @@ -381,7 +381,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 {
isOutputType,
} from './definition';
import type {
GraphQLScalarType,
GraphQLObjectType,
GraphQLInterfaceType,
GraphQLUnionType,
Expand All @@ -30,6 +32,7 @@ import type { GraphQLDirective } from './directives';
import { isIntrospectionType } from './introspection';
import { isSchema } from './schema';
import type { GraphQLSchema } from './schema';
import { isSpecifiedScalarType } from './scalars';
import find from '../jsutils/find';
import invariant from '../jsutils/invariant';
import { GraphQLError } from '../error/GraphQLError';
Expand Down Expand Up @@ -239,7 +242,10 @@ function validateTypes(context: SchemaValidationContext): void {
// Ensure they are named correctly.
validateName(context, type);

if (isObjectType(type)) {
if (isScalarType(type)) {
// Ensure ofType is a built-in scalar
validateScalarOfType(context, type);
} else if (isObjectType(type)) {
// Ensure fields are valid
validateFields(context, type);

Expand All @@ -261,6 +267,20 @@ function validateTypes(context: SchemaValidationContext): void {
});
}

function validateScalarOfType(
context: SchemaValidationContext,
type: GraphQLScalarType,
): void {
const ofType = type.ofType;
if (ofType && !isSpecifiedScalarType(ofType)) {
context.reportError(
`Type ${type.name} may only be described in terms of a built-in scalar ` +
`type. However ${ofType.name} is not a built-in scalar type.`,
type.astNode,
);
}
}

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 @@ -498,8 +498,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 @@ -508,6 +517,7 @@ describe('Type System Printer', () => {
const Root = new GraphQLObjectType({
name: 'Root',
fields: {
even: { type: EvenType },
odd: { type: OddType },
},
});
Expand All @@ -519,9 +529,12 @@ describe('Type System Printer', () => {
query: Root
}

scalar Even as Int

scalar Odd

type Root {
even: Even
odd: Odd
}
`);
Expand Down Expand Up @@ -780,10 +793,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 @@ -799,7 +812,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 @@ -1000,10 +1015,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 @@ -1018,7 +1033,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 @@ -442,6 +442,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
// value, 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
10 changes: 10 additions & 0 deletions src/utilities/buildClientSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
GraphQLEnumType,
GraphQLInputObjectType,
assertNullableType,
assertScalarType,
assertObjectType,
assertInterfaceType,
} from '../type/definition';
Expand Down Expand Up @@ -179,6 +180,11 @@ export function buildClientSchema(
return assertInterfaceType(type);
}

function getScalarType(typeRef: IntrospectionTypeRef): GraphQLScalarType {
const type = getType(typeRef);
return assertScalarType(type);
}

// Given a type's introspection result, construct the correct
// GraphQLType instance.
function buildType(type: IntrospectionType): GraphQLNamedType {
Expand Down Expand Up @@ -208,9 +214,13 @@ export function buildClientSchema(
function buildScalarDef(
scalarIntrospection: IntrospectionScalarType,
): GraphQLScalarType {
const ofType = scalarIntrospection.ofType
? getScalarType(scalarIntrospection.ofType)
: undefined;
return new GraphQLScalarType({
name: scalarIntrospection.name,
description: scalarIntrospection.description,
ofType,
serialize: value => value,
});
}
Expand Down
Loading