From e65eaddf08aae1ebd7894798a1f77394a39f92e1 Mon Sep 17 00:00:00 2001 From: Stefan Probst Date: Sat, 9 Mar 2019 16:33:56 +0100 Subject: [PATCH] fix(stitching): serialize/deserialize enum/custom scalar values BREAKING CHANGE: This change allows enums and custom scalars to be used as arguments within merged schemas. It also fixes seralization and deserialization more generally within merged schemas. If an implementation is available for a custom scalar within a merged schema (i.e., the schema is local), the internal representation will be available for use with stitching. Previously, the merged schema internally used the serialized version. --- src/stitching/checkResultAndHandleErrors.ts | 3 + src/stitching/schemaRecreation.ts | 14 ++-- src/test/testMergeSchemas.ts | 71 ++++++++++++++++++--- src/transforms/AddArgumentsAsVariables.ts | 41 ++++++++++-- 4 files changed, 106 insertions(+), 23 deletions(-) diff --git a/src/stitching/checkResultAndHandleErrors.ts b/src/stitching/checkResultAndHandleErrors.ts index 18231f0fb52..cd9fca996f1 100644 --- a/src/stitching/checkResultAndHandleErrors.ts +++ b/src/stitching/checkResultAndHandleErrors.ts @@ -5,6 +5,7 @@ import { isObjectType, isListType, isEnumType, + isScalarType, ExecutionResult, GraphQLError, } from 'graphql'; @@ -65,6 +66,8 @@ export function handleResult( if (value) { return value.value; } + } else if (isScalarType(nullableType)) { + return nullableType.parseValue(resultObject); } return resultObject; diff --git a/src/stitching/schemaRecreation.ts b/src/stitching/schemaRecreation.ts index 3c51530d93f..8c794b8e863 100644 --- a/src/stitching/schemaRecreation.ts +++ b/src/stitching/schemaRecreation.ts @@ -107,22 +107,16 @@ export function recreateType( values: newValues, }); } else if (type instanceof GraphQLScalarType) { - if (keepResolvers || isSpecifiedScalarType(type)) { + if (isSpecifiedScalarType(type)) { return type; } else { return new GraphQLScalarType({ name: type.name, description: type.description, astNode: type.astNode, - serialize(value: any) { - return value; - }, - parseValue(value: any) { - return value; - }, - parseLiteral(ast: ValueNode) { - return parseLiteral(ast); - }, + serialize: type.serialize ? type.serialize : (value: any) => value, + parseValue: type.parseValue ? type.parseValue : (value: any) => value, + parseLiteral: type.parseLiteral ? type.parseLiteral : (ast: any) => parseLiteral(ast), }); } } else { diff --git a/src/test/testMergeSchemas.ts b/src/test/testMergeSchemas.ts index c8a23245987..4343d6c9c8c 100644 --- a/src/test/testMergeSchemas.ts +++ b/src/test/testMergeSchemas.ts @@ -4,13 +4,13 @@ import { expect } from 'chai'; import { graphql, GraphQLSchema, + GraphQLField, GraphQLObjectType, GraphQLScalarType, subscribe, parse, ExecutionResult, defaultFieldResolver, - GraphQLField, findDeprecatedUsages, } from 'graphql'; import mergeSchemas from '../stitching/mergeSchemas'; @@ -72,10 +72,32 @@ let scalarTest = ` } type Query { - testingScalar: TestingScalar + testingScalar(input: TestScalar): TestingScalar } `; +let scalarSchema: GraphQLSchema; + +scalarSchema = makeExecutableSchema({ + typeDefs: scalarTest, + resolvers: { + TestScalar: new GraphQLScalarType({ + name: 'TestScalar', + description: undefined, + serialize: value => (value as string).slice(1), + parseValue: value => `_${value}`, + parseLiteral: (ast: any) => `_${ast.value}`, + }), + Query: { + testingScalar(parent, args) { + return { + value: args.input[0] === '_' ? args.input : null + }; + }, + }, + }, +}); + let enumTest = ` """ A type that uses an Enum. @@ -107,7 +129,7 @@ let enumTest = ` } type Query { - color: Color + color(input: Color): Color numericEnum: NumericEnum wrappedEnum: EnumWrapper } @@ -125,8 +147,8 @@ enumSchema = makeExecutableSchema({ TEST: 1, }, Query: { - color() { - return '#EA3232'; + color(parent, args) { + return args.input === '#EA3232' ? args.input : null; }, numericEnum() { return 1; @@ -289,8 +311,8 @@ testCombinations.forEach(async combination => { propertySchema, bookingSchema, productSchema, - scalarTest, interfaceExtensionTest, + scalarSchema, enumSchema, linkSchema, loneExtend, @@ -543,12 +565,45 @@ testCombinations.forEach(async combination => { expect(mergedResult).to.deep.equal(propertyResult); }); + it('works with custom scalars', async () => { + const scalarResult = await graphql( + scalarSchema, + ` + query { + testingScalar(input: "test") { + value + } + } + `, + ); + + const mergedResult = await graphql( + mergedSchema, + ` + query { + testingScalar(input: "test") { + value + } + } + `, + ); + + expect(scalarResult).to.deep.equal({ + data: { + testingScalar: { + value: 'test' + } + }, + }); + expect(mergedResult).to.deep.equal(scalarResult); + }); + it('works with custom enums', async () => { const enumResult = await graphql( enumSchema, ` query { - color + color(input: RED) numericEnum numericEnumInfo: __type(name: "NumericEnum") { enumValues(includeDeprecated: true) { @@ -576,7 +631,7 @@ testCombinations.forEach(async combination => { mergedSchema, ` query { - color + color(input: RED) numericEnum numericEnumInfo: __type(name: "NumericEnum") { enumValues(includeDeprecated: true) { diff --git a/src/transforms/AddArgumentsAsVariables.ts b/src/transforms/AddArgumentsAsVariables.ts index b2378ff8b64..3904c945c8d 100644 --- a/src/transforms/AddArgumentsAsVariables.ts +++ b/src/transforms/AddArgumentsAsVariables.ts @@ -3,17 +3,21 @@ import { DocumentNode, FragmentDefinitionNode, GraphQLArgument, + GraphQLEnumType, + GraphQLInputObjectType, GraphQLInputType, GraphQLList, GraphQLField, GraphQLNonNull, GraphQLObjectType, + GraphQLScalarType, GraphQLSchema, Kind, OperationDefinitionNode, SelectionNode, TypeNode, VariableDefinitionNode, + getNullableType, } from 'graphql'; import { Request } from '../Interfaces'; import { Transform } from './transforms'; @@ -62,6 +66,7 @@ function addVariablesToRootField( ) as Array; const variableNames = {}; + const newVariables = {}; const newOperations = operations.map((operation: OperationDefinitionNode) => { let existingVariables = operation.variableDefinitions.map( @@ -130,6 +135,10 @@ function addVariablesToRootField( }, type: typeToAst(argument.type), }; + newVariables[variableName] = serializeArgumentValue( + argument.type, + args[argument.name], + ); } }); @@ -154,11 +163,6 @@ function addVariablesToRootField( }; }); - const newVariables = {}; - Object.keys(variableNames).forEach(name => { - newVariables[variableNames[name]] = args[name]; - }); - return { document: { ...document, @@ -197,3 +201,30 @@ function typeToAst(type: GraphQLInputType): TypeNode { }; } } + +function serializeArgumentValue(type: GraphQLInputType, value: any): any { + if (value == null) { + return null; + } + + const nullableType = getNullableType(type); + + if (nullableType instanceof GraphQLEnumType || nullableType instanceof GraphQLScalarType) { + return nullableType.serialize(value); + } + + if (nullableType instanceof GraphQLList) { + return value.map((listMember: any) => serializeArgumentValue(nullableType.ofType, listMember)); + } + + if (nullableType instanceof GraphQLInputObjectType) { + const fields = nullableType.getFields(); + const newValue = {}; + Object.keys(value).forEach(key => { + newValue[key] = serializeArgumentValue(fields[key].type, value[key]); + }); + return newValue; + } + + return value; +}