diff --git a/src/language/__tests__/schema-kitchen-sink.graphql b/src/language/__tests__/schema-kitchen-sink.graphql index 1c7b5c3b30..aebc5a7d54 100644 --- a/src/language/__tests__/schema-kitchen-sink.graphql +++ b/src/language/__tests__/schema-kitchen-sink.graphql @@ -75,6 +75,8 @@ scalar CustomScalar scalar AnnotatedScalar @onScalar +scalar StringEncodedCustomScalar as String + extend scalar CustomScalar @onScalar enum Site { diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index 369ac9f02e..a4f23bc58c 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -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 }, }, diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index 6740d8b4c0..0071b26a3e 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -121,6 +121,8 @@ describe('Printer: SDL document', () => { scalar AnnotatedScalar @onScalar + scalar StringEncodedCustomScalar as String + extend scalar CustomScalar @onScalar enum Site { diff --git a/src/language/ast.js b/src/language/ast.js index 22169c327d..b8fdf4c522 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -423,6 +423,7 @@ export type ScalarTypeDefinitionNode = { +loc?: Location, +description?: StringValueNode, +name: NameNode, + +type?: NamedTypeNode, +directives?: $ReadOnlyArray, }; diff --git a/src/language/parser.js b/src/language/parser.js index 3802448e0a..97677cf4c6 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -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), }; diff --git a/src/language/printer.js b/src/language/printer.js index 949f7c22c7..8cf5a6ab2b 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -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( diff --git a/src/language/visitor.js b/src/language/visitor.js index ab5839ada7..16b549ddb0 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -102,7 +102,7 @@ export const QueryDocumentKeys = { SchemaDefinition: ['directives', 'operationTypes'], OperationTypeDefinition: ['type'], - ScalarTypeDefinition: ['description', 'name', 'directives'], + ScalarTypeDefinition: ['description', 'name', 'type', 'directives'], ObjectTypeDefinition: [ 'description', 'name', diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 532a19db3e..7e6384b149 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -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', }, { diff --git a/src/type/definition.js b/src/type/definition.js index 675092d0a7..bfb57d4c49 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -538,6 +538,7 @@ export class GraphQLScalarType { serialize: GraphQLScalarSerializer<*>; parseValue: GraphQLScalarValueParser<*>; parseLiteral: GraphQLScalarLiteralParser<*>; + ofType: ?GraphQLScalarType; astNode: ?ScalarTypeDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; @@ -545,10 +546,18 @@ export class GraphQLScalarType { 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', @@ -591,6 +600,7 @@ export type GraphQLScalarTypeConfig = {| parseValue?: GraphQLScalarValueParser, // Parses an externally provided literal value to use as an input. parseLiteral?: GraphQLScalarLiteralParser, + ofType?: ?GraphQLScalarType, astNode?: ?ScalarTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, |}; diff --git a/src/type/introspection.js b/src/type/introspection.js index 63630d95c9..f1b3bd98ac 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -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.', @@ -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, diff --git a/src/type/validate.js b/src/type/validate.js index 22f3423d58..db36cb9f57 100644 --- a/src/type/validate.js +++ b/src/type/validate.js @@ -8,6 +8,7 @@ */ import { + isScalarType, isObjectType, isInterfaceType, isUnionType, @@ -19,6 +20,7 @@ import { isRequiredArgument, } from './definition'; import type { + GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, @@ -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'; @@ -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); @@ -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, diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index e4a1a6c1ad..dd3efc07e1 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -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; }, @@ -473,6 +482,7 @@ describe('Type System Printer', () => { const Query = new GraphQLObjectType({ name: 'Query', fields: { + even: { type: EvenType }, odd: { type: OddType }, }, }); @@ -480,9 +490,12 @@ describe('Type System Printer', () => { 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 } `); @@ -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! @@ -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 """ @@ -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 @@ -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. diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 2319e740b3..ecf7b604a0 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -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, }); diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index 531036dca3..3fe9a2c539 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -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, }); } diff --git a/src/utilities/findBreakingChanges.js b/src/utilities/findBreakingChanges.js index 9f7962138f..1b27e0775b 100644 --- a/src/utilities/findBreakingChanges.js +++ b/src/utilities/findBreakingChanges.js @@ -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; diff --git a/src/utilities/introspectionQuery.js b/src/utilities/introspectionQuery.js index b6ebe58485..1ad3a4a7e4 100644 --- a/src/utilities/introspectionQuery.js +++ b/src/utilities/introspectionQuery.js @@ -155,6 +155,7 @@ export type IntrospectionScalarType = { +kind: 'SCALAR', +name: string, +description?: ?string, + +ofType?: ?IntrospectionNamedTypeRef, }; export type IntrospectionObjectType = { diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index aa9b01c096..dd6665c650 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -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 {