Skip to content
This repository has been archived by the owner on Apr 15, 2020. It is now read-only.

Commit

Permalink
fix(stitching): add default value support
Browse files Browse the repository at this point in the history
  • Loading branch information
yaacovCR committed Dec 31, 2019
1 parent 5ffcc4f commit f37ff4c
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 154 deletions.
18 changes: 0 additions & 18 deletions src/Interfaces.ts
Expand Up @@ -8,7 +8,6 @@ import {
GraphQLIsTypeOfFn,
GraphQLTypeResolver,
GraphQLScalarType,
GraphQLNamedType,
DocumentNode,
ASTNode,
} from 'graphql';
Expand Down Expand Up @@ -177,23 +176,6 @@ export interface IMockServer {
) => Promise<ExecutionResult>;
}

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<MergeTypeCandidate>,
) => VisitTypeResult;

export type Operation = 'query' | 'mutation' | 'subscription';

export type Request = {
Expand Down
89 changes: 45 additions & 44 deletions src/generate/addResolveFunctionsToSchema.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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];
Expand All @@ -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.`,
);
}

Expand All @@ -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;
Expand All @@ -111,53 +104,61 @@ 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);

// 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;
Expand Down
82 changes: 29 additions & 53 deletions src/stitching/mergeSchemas.ts
Expand Up @@ -18,9 +18,6 @@ import {
IFieldResolver,
IResolvers,
MergeInfo,
MergeTypeCandidate,
TypeWithResolvers,
VisitTypeResult,
IResolversParameter,
} from '../Interfaces';
import {
Expand All @@ -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?: {
Expand Down Expand Up @@ -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<GraphQLNamedType>
>;
onTypeConflict?: OnTypeConflict;
resolvers?: IResolversParameter;
schemaDirectives?: { [name: string]: typeof SchemaDirectiveVisitor };
inheritResolversFromInterfaces?: boolean;
mergeDirectives?: boolean,

}): GraphQLSchema {
const allSchemas: Array<GraphQLSchema> = [];
const typeCandidates: { [name: string]: Array<MergeTypeCandidate> } = {};
Expand Down Expand Up @@ -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(<GraphQLNamedType>resultType)) {
type = <GraphQLNamedType>resultType;
} else if ((<TypeWithResolvers>resultType).type) {
type = (<TypeWithResolvers>resultType).type;
typeResolvers = (<TypeWithResolvers>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;
}
});

Expand Down Expand Up @@ -473,11 +446,11 @@ function onTypeConflictToCandidateSelector(onTypeConflict: OnTypeConflict): Cand
});
}

function defaultVisitType(
function mergeTypeCandidates(
name: string,
candidates: Array<MergeTypeCandidate>,
candidateSelector?: CandidateSelector
) {
): MergeTypeCandidatesResult {
if (!candidateSelector) {
candidateSelector = cands => cands[cands.length - 1];
}
Expand Down Expand Up @@ -524,6 +497,9 @@ function defaultVisitType(
};
} else {
const candidate = candidateSelector(candidates);
return candidate.type;
return {
type: candidate.type,
candidate
};
}
}
28 changes: 24 additions & 4 deletions src/stitching/schemaRecreation.ts
Expand Up @@ -9,6 +9,7 @@ import {
GraphQLFieldMap,
GraphQLInputField,
GraphQLInputFieldConfig,
GraphQLInputType,
GraphQLInputFieldConfigMap,
GraphQLInputFieldMap,
GraphQLInputObjectType,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
},
];
}
Expand All @@ -292,10 +300,22 @@ export function inputFieldToFieldConfig(
field: GraphQLInputField,
resolveType: ResolveType<any>,
): 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));
}
9 changes: 7 additions & 2 deletions src/stitching/typeFromAST.ts
Expand Up @@ -21,7 +21,6 @@ import {
ScalarTypeDefinitionNode,
TypeNode,
UnionTypeDefinitionNode,
valueFromAST,
getDescription,
GraphQLString,
GraphQLDirective,
Expand Down Expand Up @@ -183,7 +182,7 @@ function makeValues(nodes: ReadonlyArray<InputValueDefinitionNode>) {
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),
};
});
Expand Down Expand Up @@ -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<DirectiveLocationEnum> = [];
node.locations.forEach(location => {
Expand Down

0 comments on commit f37ff4c

Please sign in to comment.