diff --git a/src/language/__tests__/schema-kitchen-sink.graphql b/src/language/__tests__/schema-kitchen-sink.graphql index f94f47c8e5..20fe9b48b6 100644 --- a/src/language/__tests__/schema-kitchen-sink.graphql +++ b/src/language/__tests__/schema-kitchen-sink.graphql @@ -65,6 +65,8 @@ extend union Feed @onUnion scalar CustomScalar +scalar StringEncodedCustomScalar as String + scalar AnnotatedScalar @onScalar extend scalar CustomScalar @onScalar diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index 1c9fe0f144..9eb879ba05 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -712,6 +712,7 @@ type Hello { { kind: 'ScalarTypeDefinition', name: nameNode('Hello', { start: 7, end: 12 }), + 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 1bacb8e586..b6324e5b1c 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -109,6 +109,8 @@ describe('Printer', () => { scalar CustomScalar + scalar StringEncodedCustomScalar as String + scalar AnnotatedScalar @onScalar extend scalar CustomScalar @onScalar diff --git a/src/language/ast.js b/src/language/ast.js index bec5ae3d57..395fd78132 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -448,6 +448,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 afa5d6ecab..a60ba1a2c7 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -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), }; @@ -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) { diff --git a/src/language/printer.js b/src/language/printer.js index 10bb63595a..a7d94e9f69 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -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, diff --git a/src/language/visitor.js b/src/language/visitor.js index 31937548ee..0148c96aae 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -101,7 +101,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 e47aea1081..b1ad489ced 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -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', }, { diff --git a/src/type/definition.js b/src/type/definition.js index 2e54f99ca2..f17bc90adf 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -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.'); @@ -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; } @@ -492,7 +496,9 @@ export class GraphQLScalarType { // Parses an externally provided literal value to use as an input. parseLiteral(valueNode: ValueNode, variables: ?ObjMap): mixed { - const parser = this._scalarConfig.parseLiteral; + const parser = + this._scalarConfig.parseLiteral || + (this.ofType && this.ofType.parseLiteral); return parser ? parser(valueNode, variables) : valueFromASTUntyped(valueNode, variables); @@ -513,6 +519,7 @@ GraphQLScalarType.prototype.toJSON = GraphQLScalarType.prototype.inspect = export type GraphQLScalarTypeConfig = { name: string, description?: ?string, + ofType?: ?GraphQLScalarType, astNode?: ?ScalarTypeDefinitionNode, serialize: (value: mixed) => ?TExternal, parseValue?: (value: mixed) => ?TInternal, diff --git a/src/type/introspection.js b/src/type/introspection.js index 759f4b8f2b..907368c01a 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -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.', @@ -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, diff --git a/src/type/validate.js b/src/type/validate.js index bf6ec8315a..eab5779851 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 { isOutputType, } from './definition'; import type { + GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, @@ -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'; @@ -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); @@ -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, diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 96a6a21104..9748251deb 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -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; }, @@ -508,6 +517,7 @@ describe('Type System Printer', () => { const Root = new GraphQLObjectType({ name: 'Root', fields: { + even: { type: EvenType }, odd: { type: OddType }, }, }); @@ -519,9 +529,12 @@ describe('Type System Printer', () => { query: Root } + scalar Even as Int + scalar Odd type Root { + even: Even odd: Odd } `); @@ -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! @@ -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 """ @@ -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 @@ -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. diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 16ed9a99f4..27690f6882 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -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, }); diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index cc883b3a88..a8ee367249 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -26,6 +26,7 @@ import { GraphQLEnumType, GraphQLInputObjectType, assertNullableType, + assertScalarType, assertObjectType, assertInterfaceType, } from '../type/definition'; @@ -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 { @@ -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, }); } diff --git a/src/utilities/findBreakingChanges.js b/src/utilities/findBreakingChanges.js index b5787df2d4..61a3ea2185 100644 --- a/src/utilities/findBreakingChanges.js +++ b/src/utilities/findBreakingChanges.js @@ -161,6 +161,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 4071250998..26ca0867a5 100644 --- a/src/utilities/introspectionQuery.js +++ b/src/utilities/introspectionQuery.js @@ -150,6 +150,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 568274e148..84249933ce 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -170,7 +170,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 {