diff --git a/src/Interfaces.ts b/src/Interfaces.ts index a9cca8d74d6..34a5e8c4438 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -8,7 +8,6 @@ import { GraphQLIsTypeOfFn, GraphQLTypeResolver, GraphQLScalarType, - GraphQLNamedType, DocumentNode, ASTNode, } from 'graphql'; @@ -177,23 +176,6 @@ export interface IMockServer { ) => Promise; } -export type MergeTypeCandidate = { - schema?: GraphQLSchema; - type: GraphQLNamedType; -}; - -export type TypeWithResolvers = { - type: GraphQLNamedType; - resolvers?: IResolvers; -}; - -export type VisitTypeResult = GraphQLNamedType | TypeWithResolvers | null; - -export type VisitType = ( - name: string, - candidates: Array, -) => VisitTypeResult; - export type Operation = 'query' | 'mutation' | 'subscription'; export type Request = { diff --git a/src/generate/addResolveFunctionsToSchema.ts b/src/generate/addResolveFunctionsToSchema.ts index 5e9741017af..0cb6f729cec 100644 --- a/src/generate/addResolveFunctionsToSchema.ts +++ b/src/generate/addResolveFunctionsToSchema.ts @@ -18,7 +18,7 @@ import { } from '../Interfaces'; import { applySchemaTransforms } from '../transforms/transforms'; import { checkForResolveTypeResolver, extendResolversFromInterfaces } from '.'; -import ConvertEnumValues from '../transforms/ConvertEnumValues'; +import AddEnumAndScalarResolvers from '../transforms/AddEnumAndScalarResolvers'; function addResolveFunctionsToSchema( options: IAddResolveFunctionsToSchemaOptions | GraphQLSchema, @@ -55,6 +55,8 @@ function addResolveFunctionsToSchema( // Used to map the external value of an enum to its internal value, when // that internal value is provided by a resolver. const enumValueMap = Object.create(null); + // Used to store custom scalar implementations. + const scalarTypeMap = Object.create(null); Object.keys(resolvers).forEach(typeName => { const resolverValue = resolvers[typeName]; @@ -63,7 +65,7 @@ function addResolveFunctionsToSchema( if (resolverType !== 'object' && resolverType !== 'function') { throw new SchemaError( `"${typeName}" defined in resolvers, but has invalid value "${resolverValue}". A resolver's value ` + - `must be of type object or function.`, + `must be of type object or function.`, ); } @@ -79,19 +81,10 @@ function addResolveFunctionsToSchema( ); } - Object.keys(resolverValue).forEach(fieldName => { - if (fieldName.startsWith('__')) { - // this is for isTypeOf and resolveType and all the other stuff. - type[fieldName.substring(2)] = resolverValue[fieldName]; - return; - } - - if (type instanceof GraphQLScalarType) { - type[fieldName] = resolverValue[fieldName]; - return; - } - - if (type instanceof GraphQLEnumType) { + if (type instanceof GraphQLScalarType) { + scalarTypeMap[type.name] = resolverValue; + } else if (type instanceof GraphQLEnumType) { + Object.keys(resolverValue).forEach(fieldName => { if (!type.getValue(fieldName)) { if (allowResolversNotInSchema) { return; @@ -111,44 +104,51 @@ function addResolveFunctionsToSchema( // internal value. enumValueMap[type.name] = enumValueMap[type.name] || {}; enumValueMap[type.name][fieldName] = resolverValue[fieldName]; - return; - } - + }); + } else { // object type - const fields = getFieldsForType(type); - if (!fields) { - if (allowResolversNotInSchema) { + Object.keys(resolverValue).forEach(fieldName => { + if (fieldName.startsWith('__')) { + // this is for isTypeOf and resolveType and all the other stuff. + type[fieldName.substring(2)] = resolverValue[fieldName]; return; } - throw new SchemaError( - `${typeName} was defined in resolvers, but it's not an object`, - ); - } + const fields = getFieldsForType(type); + if (!fields) { + if (allowResolversNotInSchema) { + return; + } - if (!fields[fieldName]) { - if (allowResolversNotInSchema) { - return; + throw new SchemaError( + `${typeName} was defined in resolvers, but it's not an object`, + ); } - throw new SchemaError( - `${typeName}.${fieldName} defined in resolvers, but not in schema`, - ); - } - const field = fields[fieldName]; - const fieldResolve = resolverValue[fieldName]; - if (typeof fieldResolve === 'function') { - // for convenience. Allows shorter syntax in resolver definition file - setFieldProperties(field, { resolve: fieldResolve }); - } else { - if (typeof fieldResolve !== 'object') { + if (!fields[fieldName]) { + if (allowResolversNotInSchema) { + return; + } + throw new SchemaError( - `Resolver ${typeName}.${fieldName} must be object or function`, + `${typeName}.${fieldName} defined in resolvers, but not in schema`, ); } - setFieldProperties(field, fieldResolve); - } - }); + const field = fields[fieldName]; + const fieldResolve = resolverValue[fieldName]; + if (typeof fieldResolve === 'function') { + // for convenience. Allows shorter syntax in resolver definition file + setFieldProperties(field, { resolve: fieldResolve }); + } else { + if (typeof fieldResolve !== 'object') { + throw new SchemaError( + `Resolver ${typeName}.${fieldName} must be object or function`, + ); + } + setFieldProperties(field, fieldResolve); + } + }); + } }); checkForResolveTypeResolver(schema, requireResolversForResolveType); @@ -156,8 +156,9 @@ function addResolveFunctionsToSchema( // If there are any enum resolver functions (that are used to return // internal enum values), create a new schema that includes enums with the // new internal facing values. + // also parse all defaultValues in all input fields to use internal values for enums/scalars const updatedSchema = applySchemaTransforms(schema, [ - new ConvertEnumValues(enumValueMap), + new AddEnumAndScalarResolvers(enumValueMap, scalarTypeMap), ]); return updatedSchema; diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index d936887e3f9..5b0371ae62b 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -18,9 +18,6 @@ import { IFieldResolver, IResolvers, MergeInfo, - MergeTypeCandidate, - TypeWithResolvers, - VisitTypeResult, IResolversParameter, } from '../Interfaces'; import { @@ -43,7 +40,18 @@ import { import mergeDeep from '../mergeDeep'; import { SchemaDirectiveVisitor } from '../schemaVisitor'; -export type OnTypeConflict = ( +type MergeTypeCandidate = { + schema?: GraphQLSchema; + type: GraphQLNamedType; +}; + +type MergeTypeCandidatesResult = { + type?: GraphQLNamedType; + resolvers?: IResolvers; + candidate?: MergeTypeCandidate; +}; + +type OnTypeConflict = ( left: GraphQLNamedType, right: GraphQLNamedType, info?: { @@ -76,35 +84,6 @@ export default function mergeSchemas({ schemaDirectives?: { [name: string]: typeof SchemaDirectiveVisitor }; inheritResolversFromInterfaces?: boolean; mergeDirectives?: boolean, - -}): GraphQLSchema { - return mergeSchemasImplementation({ - schemas, - onTypeConflict, - resolvers, - schemaDirectives, - inheritResolversFromInterfaces, - mergeDirectives, - }); -} - -function mergeSchemasImplementation({ - schemas, - onTypeConflict, - resolvers, - schemaDirectives, - inheritResolversFromInterfaces, - mergeDirectives, -}: { - schemas: Array< - string | GraphQLSchema | DocumentNode | Array - >; - onTypeConflict?: OnTypeConflict; - resolvers?: IResolversParameter; - schemaDirectives?: { [name: string]: typeof SchemaDirectiveVisitor }; - inheritResolversFromInterfaces?: boolean; - mergeDirectives?: boolean, - }): GraphQLSchema { const allSchemas: Array = []; const typeCandidates: { [name: string]: Array } = {}; @@ -229,28 +208,22 @@ function mergeSchemasImplementation({ let generatedResolvers = {}; Object.keys(typeCandidates).forEach(typeName => { - const resultType: VisitTypeResult = defaultVisitType( + const mergeResult: MergeTypeCandidatesResult = mergeTypeCandidates( typeName, typeCandidates[typeName], onTypeConflict ? onTypeConflictToCandidateSelector(onTypeConflict) : undefined ); - if (resultType === null) { - types[typeName] = null; + let type: GraphQLNamedType; + let typeResolvers: IResolvers; + if (mergeResult.type) { + type = mergeResult.type; + typeResolvers = mergeResult.resolvers; } else { - let type: GraphQLNamedType; - let typeResolvers: IResolvers; - if (isNamedType(resultType)) { - type = resultType; - } else if ((resultType).type) { - type = (resultType).type; - typeResolvers = (resultType).resolvers; - } else { - throw new Error(`Invalid visitType result for type ${typeName}`); - } - types[typeName] = recreateType(type, resolveType, false); - if (typeResolvers) { - generatedResolvers[typeName] = typeResolvers; - } + throw new Error(`Invalid mergeTypeCandidates result for type ${typeName}`); + } + types[typeName] = recreateType(type, resolveType, false); + if (typeResolvers) { + generatedResolvers[typeName] = typeResolvers; } }); @@ -473,11 +446,11 @@ function onTypeConflictToCandidateSelector(onTypeConflict: OnTypeConflict): Cand }); } -function defaultVisitType( +function mergeTypeCandidates( name: string, candidates: Array, candidateSelector?: CandidateSelector -) { +): MergeTypeCandidatesResult { if (!candidateSelector) { candidateSelector = cands => cands[cands.length - 1]; } @@ -524,6 +497,9 @@ function defaultVisitType( }; } else { const candidate = candidateSelector(candidates); - return candidate.type; + return { + type: candidate.type, + candidate + }; } } diff --git a/src/stitching/schemaRecreation.ts b/src/stitching/schemaRecreation.ts index 8c794b8e863..7f37c4b5601 100644 --- a/src/stitching/schemaRecreation.ts +++ b/src/stitching/schemaRecreation.ts @@ -9,6 +9,7 @@ import { GraphQLFieldMap, GraphQLInputField, GraphQLInputFieldConfig, + GraphQLInputType, GraphQLInputFieldConfigMap, GraphQLInputFieldMap, GraphQLInputObjectType, @@ -35,6 +36,12 @@ import isSpecifiedScalarType from '../isSpecifiedScalarType'; import { ResolveType } from '../Interfaces'; import resolveFromParentTypename from './resolveFromParentTypename'; import defaultMergedResolver from './defaultMergedResolver'; +import { isStub } from './typeFromAST'; +import { + serializeInputValue, + parseInputValue, + parseInputValueLiteral +} from '../transformInputValue'; export function recreateType( type: GraphQLNamedType, @@ -265,9 +272,10 @@ export function argumentToArgumentConfig( return [ argument.name, { - type: type, - defaultValue: argument.defaultValue, + type, + defaultValue: reparseDefaultValue(argument.defaultValue, argument.type, type), description: argument.description, + astNode: argument.astNode, }, ]; } @@ -292,10 +300,22 @@ export function inputFieldToFieldConfig( field: GraphQLInputField, resolveType: ResolveType, ): GraphQLInputFieldConfig { + const type = resolveType(field.type); return { - type: resolveType(field.type), - defaultValue: field.defaultValue, + type, + defaultValue: reparseDefaultValue(field.defaultValue, field.type, type), description: field.description, astNode: field.astNode, }; } + +function reparseDefaultValue( + originalDefaultValue: any, + originalType: GraphQLInputType, + newType: GraphQLInputType, +) { + if (originalType instanceof GraphQLInputObjectType && isStub(originalType)) { + return parseInputValueLiteral(newType, originalDefaultValue); + } + return parseInputValue(newType, serializeInputValue(originalType, originalDefaultValue)); +} diff --git a/src/stitching/typeFromAST.ts b/src/stitching/typeFromAST.ts index 69367beefb3..878487fe2e2 100644 --- a/src/stitching/typeFromAST.ts +++ b/src/stitching/typeFromAST.ts @@ -21,7 +21,6 @@ import { ScalarTypeDefinitionNode, TypeNode, UnionTypeDefinitionNode, - valueFromAST, getDescription, GraphQLString, GraphQLDirective, @@ -183,7 +182,7 @@ function makeValues(nodes: ReadonlyArray) { const type = resolveType(node.type, 'input') as GraphQLInputType; result[node.name.value] = { type, - defaultValue: valueFromAST(node.defaultValue, type), + defaultValue: node.defaultValue, description: getDescription(node, backcompatOptions), }; }); @@ -227,6 +226,12 @@ function createNamedStub( }); } +export function isStub(type: GraphQLObjectType | GraphQLInputObjectType | GraphQLInterfaceType): boolean { + const fields = type.getFields(); + const fieldNames = Object.keys(fields); + return fieldNames.length === 1 && fields[fieldNames[0]].name === '__fake'; +} + function makeDirective(node: DirectiveDefinitionNode): GraphQLDirective { const locations: Array = []; node.locations.forEach(location => { diff --git a/src/test/testAlternateMergeSchemas.ts b/src/test/testAlternateMergeSchemas.ts index 0d31b2b3c70..dd613cae8c6 100644 --- a/src/test/testAlternateMergeSchemas.ts +++ b/src/test/testAlternateMergeSchemas.ts @@ -9,7 +9,9 @@ import { parse, GraphQLField, GraphQLNamedType, - FieldNode + GraphQLScalarType, + FieldNode, + printSchema } from 'graphql'; import mergeSchemas from '../stitching/mergeSchemas'; import { @@ -536,6 +538,113 @@ describe('mergeSchemas', () => { expect(response.data.test).to.be.null; expect(response.errors).to.be.undefined; }); + + it('can merge default input types', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + input InputWithDefault { + field: String = "test" + } + type Query { + getInput(input: InputWithDefault!): String + } + `, + resolvers: { + Query: { + getInput: (root, args) => args.input.field + } + } + }); + const mergedSchema = mergeSchemas({ + schemas: [schema] + }); + + const query = `{ getInput(input: {}) }`; + const response = await graphql(mergedSchema, query); + + expect(printSchema(schema)).to.equal(printSchema(mergedSchema)); + expect(response.data.getInput).to.equal('test'); + }); + + it('can override scalars with new internal values', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + scalar TestScalar + type Query { + getTestScalar: TestScalar + } + `, + resolvers: { + TestScalar: new GraphQLScalarType({ + name: 'TestScalar', + description: undefined, + serialize: value => (value as string).slice(1), + parseValue: value => `_${value}`, + parseLiteral: (ast: any) => `_${ast.value}`, + }), + Query: { + getTestScalar: () => '_test' + } + } + }); + const mergedSchema = mergeSchemas({ + schemas: [schema], + resolvers: { + TestScalar: new GraphQLScalarType({ + name: 'TestScalar', + description: undefined, + serialize: value => (value as string).slice(2), + parseValue: value => `__${value}`, + parseLiteral: (ast: any) => `__${ast.value}`, + }) + } + }); + + const query = `{ getTestScalar }`; + const response = await graphql(mergedSchema, query); + + expect(response.data.getTestScalar).to.equal('test'); + }); + + it('can override scalars with new internal values when using default input types', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + scalar TestScalar + type Query { + getTestScalar(input: TestScalar = "test"): TestScalar + } + `, + resolvers: { + TestScalar: new GraphQLScalarType({ + name: 'TestScalar', + description: undefined, + serialize: value => (value as string).slice(1), + parseValue: value => `_${value}`, + parseLiteral: (ast: any) => `_${ast.value}`, + }), + Query: { + getTestScalar: (root, args) => '_test' + } + } + }); + const mergedSchema = mergeSchemas({ + schemas: [schema], + resolvers: { + TestScalar: new GraphQLScalarType({ + name: 'TestScalar', + description: undefined, + serialize: value => (value as string).slice(2), + parseValue: value => `__${value}`, + parseLiteral: (ast: any) => `__${ast.value}`, + }) + } + }); + + const query = `{ getTestScalar }`; + const response = await graphql(mergedSchema, query); + + expect(response.data.getTestScalar).to.equal('test'); + }); }); describe('onTypeConflict', () => { diff --git a/src/transformInputValue.ts b/src/transformInputValue.ts new file mode 100644 index 00000000000..f293d625c8e --- /dev/null +++ b/src/transformInputValue.ts @@ -0,0 +1,57 @@ +import { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLInputType, + GraphQLList, + GraphQLScalarType, + getNullableType, +} from 'graphql'; + +type InputValueTransformer = (type: GraphQLEnumType | GraphQLScalarType, originalValue: any) => any; + +export function transformInputValue(type: GraphQLInputType, value: any, transformer: InputValueTransformer) { + if (value == null) { + return null; + } + + const nullableType = getNullableType(type); + + if (nullableType instanceof GraphQLEnumType || nullableType instanceof GraphQLScalarType) { + return transformer(nullableType, value); + } else if (nullableType instanceof GraphQLList) { + return value.map((listMember: any) => transformInputValue(nullableType.ofType, listMember, transformer)); + } else if (nullableType instanceof GraphQLInputObjectType) { + const fields = nullableType.getFields(); + const newValue = {}; + Object.keys(value).forEach(key => { + newValue[key] = transformInputValue(fields[key].type, value[key], transformer); + }); + return newValue; + } + + //unreachable, no other possible return value +} + +export function serializeInputValue(type: GraphQLInputType, value: any) { + return transformInputValue( + type, + value, + (t, v) => t.serialize(v) + ); +} + +export function parseInputValue(type: GraphQLInputType, value: any) { + return transformInputValue( + type, + value, + (t, v) => t.parseValue(v) + ); +} + +export function parseInputValueLiteral(type: GraphQLInputType, value: any) { + return transformInputValue( + type, + value, + (t, v) => t.parseLiteral(v, {}) + ); +} diff --git a/src/transforms/AddArgumentsAsVariables.ts b/src/transforms/AddArgumentsAsVariables.ts index 3904c945c8d..61bdd442b27 100644 --- a/src/transforms/AddArgumentsAsVariables.ts +++ b/src/transforms/AddArgumentsAsVariables.ts @@ -3,24 +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'; +import { serializeInputValue } from '../transformInputValue'; export default class AddArgumentsAsVariablesTransform implements Transform { private schema: GraphQLSchema; @@ -135,7 +132,7 @@ function addVariablesToRootField( }, type: typeToAst(argument.type), }; - newVariables[variableName] = serializeArgumentValue( + newVariables[variableName] = serializeInputValue( argument.type, args[argument.name], ); @@ -201,30 +198,3 @@ 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; -} diff --git a/src/transforms/AddEnumAndScalarResolvers.ts b/src/transforms/AddEnumAndScalarResolvers.ts new file mode 100644 index 00000000000..c7b6c7ae383 --- /dev/null +++ b/src/transforms/AddEnumAndScalarResolvers.ts @@ -0,0 +1,89 @@ +import { + GraphQLSchema, + GraphQLEnumType, + GraphQLScalarType, + GraphQLScalarTypeConfig +} from 'graphql'; +import { Transform } from './transforms'; +import { visitSchema, VisitSchemaKind } from './visitSchema'; + +export default class AddEnumAndScalarResolvers implements Transform { + private enumValueMap: object; + private scalarTypeMap: object; + + constructor(enumValueMap: object, scalarTypeMap: object) { + this.enumValueMap = enumValueMap; + this.scalarTypeMap = scalarTypeMap; + } + + public transformSchema(schema: GraphQLSchema): GraphQLSchema { + const { enumValueMap, scalarTypeMap } = this; + const enumTypeMap = Object.create(null); + + if (!Object.keys(enumValueMap).length && !Object.keys(scalarTypeMap).length) { + return schema; + } + + // Build enum types from the resolver map. + Object.keys(enumValueMap).forEach(typeName => { + const enumType: GraphQLEnumType = schema.getType(typeName) as GraphQLEnumType; + const externalToInternalValueMap = enumValueMap[enumType.name]; + + if (externalToInternalValueMap) { + const values = enumType.getValues(); + const newValues = {}; + values.forEach(value => { + const newValue = Object.keys(externalToInternalValueMap).includes( + value.name, + ) + ? externalToInternalValueMap[value.name] + : value.name; + newValues[value.name] = { + value: newValue, + deprecationReason: value.deprecationReason, + description: value.description, + astNode: value.astNode, + }; + }); + + enumTypeMap[typeName] = new GraphQLEnumType({ + name: enumType.name, + description: enumType.description, + astNode: enumType.astNode, + values: newValues, + }); + } + }); + + //Build scalar types from resolver map (if necessary, see below). + Object.keys(scalarTypeMap).forEach(typeName => { + const type = scalarTypeMap[typeName]; + + // Below is necessary as legacy code for scalar type specification allowed + // hardcoding within the resolver an object with fields 'serialize', + // 'parse', and '__parseLiteral', see examples in testMocking.ts. + if (!(type instanceof GraphQLScalarType)) { + const scalarTypeConfig = {}; + Object.keys(type).forEach(key => { + scalarTypeConfig[key.slice(2)] = type[key]; + }); + scalarTypeMap[typeName] = new GraphQLScalarType({ + name: typeName, + ...scalarTypeConfig + } as GraphQLScalarTypeConfig); + } + }); + + // type recreation within visitSchema will automatically adjust default + // values on fields. + const transformedSchema = visitSchema(schema, { + [VisitSchemaKind.SCALAR_TYPE](type: GraphQLScalarType) { + return scalarTypeMap[type.name]; + }, + [VisitSchemaKind.ENUM_TYPE](type: GraphQLEnumType) { + return enumTypeMap[type.name]; }, + }); + + return transformedSchema; + } +}