Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recreate enums and scalars in mergeSchema #613

Merged
merged 2 commits into from
Feb 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Change log

### vNEXT
* Recreate enums and scalars for more consistent behaviour of merged schemas [PR #613](https://github.com/apollographql/graphql-tools/pull/613)
* `makeExecutableSchema` and `mergeSchema` now accept an array of `IResolver` [PR #612](https://github.com/apollographql/graphql-tools/pull/612) [PR #576](https://github.com/apollographql/graphql-tools/pull/576) [PR #577](https://github.com/apollographql/graphql-tools/pull/577)
* Fix `delegateToSchema.ts` to remove duplicate new variable definitions when delegating to schemas [PR #607](https://github.com/apollographql/graphql-tools/pull/607)
* Fix duplicate subscriptions for schema stitching [PR #609](https://github.com/apollographql/graphql-tools/pull/609)
Expand Down
2 changes: 2 additions & 0 deletions src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,5 @@ export interface IMockServer {
vars?: { [key: string]: any },
) => Promise<ExecutionResult>;
}

export type ResolveType<T extends GraphQLType> = (type: T) => T;
25 changes: 10 additions & 15 deletions src/stitching/mergeSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
DocumentNode,
GraphQLField,
GraphQLFieldMap,
GraphQLInputObjectType,
GraphQLNamedType,
GraphQLObjectType,
GraphQLResolveInfo,
Expand All @@ -12,7 +11,6 @@ import {
buildASTSchema,
extendSchema,
getNamedType,
isCompositeType,
isNamedType,
parse,
} from 'graphql';
Expand All @@ -30,8 +28,9 @@ import {
addResolveFunctionsToSchema,
} from '../schemaGenerator';
import {
recreateCompositeType,
recreateType,
fieldMapToFieldConfigMap,
createResolveType,
} from './schemaRecreation';
import delegateToSchema from './delegateToSchema';
import typeFromAST from './typeFromAST';
Expand Down Expand Up @@ -59,6 +58,10 @@ export default function mergeSchemas({

const typeRegistry = new TypeRegistry();

const resolveType = createResolveType(name => {
return typeRegistry.getType(name);
});

const mergeInfo: MergeInfo = createMergeInfo(typeRegistry);

const actualSchemas: Array<GraphQLSchema> = [];
Expand Down Expand Up @@ -106,15 +109,7 @@ export default function mergeSchemas({
type !== mutationType &&
type !== subscriptionType
) {
let newType;
if (isCompositeType(type) || type instanceof GraphQLInputObjectType) {
newType = recreateCompositeType(schema, type, typeRegistry);
} else {
newType = getNamedType(type);
}
if (newType instanceof GraphQLObjectType) {
delete newType.isTypeOf;
}
const newType = recreateType(type, resolveType);
typeRegistry.addType(newType.name, newType, onTypeConflict);
}
});
Expand Down Expand Up @@ -212,22 +207,22 @@ export default function mergeSchemas({

const query = new GraphQLObjectType({
name: 'Query',
fields: () => fieldMapToFieldConfigMap(queryFields, typeRegistry),
fields: () => fieldMapToFieldConfigMap(queryFields, resolveType),
});

let mutation;
if (!isEmptyObject(mutationFields)) {
mutation = new GraphQLObjectType({
name: 'Mutation',
fields: () => fieldMapToFieldConfigMap(mutationFields, typeRegistry),
fields: () => fieldMapToFieldConfigMap(mutationFields, resolveType),
});
}

let subscription;
if (!isEmptyObject(subscriptionFields)) {
subscription = new GraphQLObjectType({
name: 'Subscription',
fields: () => fieldMapToFieldConfigMap(subscriptionFields, typeRegistry),
fields: () => fieldMapToFieldConfigMap(subscriptionFields, resolveType),
});
}

Expand Down
167 changes: 139 additions & 28 deletions src/stitching/schemaRecreation.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,53 @@
import {
GraphQLArgument,
GraphQLArgumentConfig,
GraphQLCompositeType,
GraphQLBoolean,
GraphQLEnumType,
GraphQLField,
GraphQLFieldConfig,
GraphQLFieldConfigArgumentMap,
GraphQLFieldConfigMap,
GraphQLFieldMap,
GraphQLFloat,
GraphQLID,
GraphQLInputField,
GraphQLInputFieldConfig,
GraphQLInputFieldConfigMap,
GraphQLInputFieldMap,
GraphQLInputObjectType,
GraphQLInt,
GraphQLInterfaceType,
GraphQLList,
GraphQLNamedType,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
GraphQLScalarType,
GraphQLString,
GraphQLType,
GraphQLUnionType,
Kind,
ValueNode,
getNamedType,
isNamedType,
} from 'graphql';
import TypeRegistry from './TypeRegistry';
import { ResolveType } from '../Interfaces';
import resolveFromParentTypename from './resolveFromParentTypename';
import defaultMergedResolver from './defaultMergedResolver';

export function recreateCompositeType(
schema: GraphQLSchema,
type: GraphQLCompositeType | GraphQLInputObjectType,
registry: TypeRegistry,
): GraphQLCompositeType | GraphQLInputObjectType {
export function recreateType(
type: GraphQLNamedType,
resolveType: ResolveType<any>,
): GraphQLNamedType {
if (type instanceof GraphQLObjectType) {
const fields = type.getFields();
const interfaces = type.getInterfaces();

return new GraphQLObjectType({
name: type.name,
description: type.description,
isTypeOf: type.isTypeOf,
astNode: type.astNode,
fields: () => fieldMapToFieldConfigMap(fields, registry),
interfaces: () => interfaces.map(iface => registry.resolveType(iface)),
fields: () => fieldMapToFieldConfigMap(fields, resolveType),
interfaces: () => interfaces.map(iface => resolveType(iface)),
});
} else if (type instanceof GraphQLInterfaceType) {
const fields = type.getFields();
Expand All @@ -45,7 +56,7 @@ export function recreateCompositeType(
name: type.name,
description: type.description,
astNode: type.astNode,
fields: () => fieldMapToFieldConfigMap(fields, registry),
fields: () => fieldMapToFieldConfigMap(fields, resolveType),
resolveType: (parent, context, info) =>
resolveFromParentTypename(parent, info.schema),
});
Expand All @@ -54,8 +65,8 @@ export function recreateCompositeType(
name: type.name,
description: type.description,
astNode: type.astNode,
types: () =>
type.getTypes().map(unionMember => registry.resolveType(unionMember)),

types: () => type.getTypes().map(unionMember => resolveType(unionMember)),
resolveType: (parent, context, info) =>
resolveFromParentTypename(parent, info.schema),
});
Expand All @@ -64,31 +75,127 @@ export function recreateCompositeType(
name: type.name,
description: type.description,
astNode: type.astNode,
fields: () => inputFieldMapToFieldConfigMap(type.getFields(), registry),

fields: () =>
inputFieldMapToFieldConfigMap(type.getFields(), resolveType),
});
} else if (type instanceof GraphQLEnumType) {
const values = type.getValues();
const newValues = {};
values.forEach(value => {
newValues[value.name] = { value: value.name };
});
return new GraphQLEnumType({
name: type.name,
description: type.description,
astNode: type.astNode,
values: newValues,
});
} else if (type instanceof GraphQLScalarType) {
if (
type === GraphQLID ||
type === GraphQLString ||
type === GraphQLFloat ||
type === GraphQLBoolean ||
type === GraphQLInt
) {
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);
},
});
}
} else {
throw new Error(`Invalid type ${type}`);
}
}

function parseLiteral(ast: ValueNode): any {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN: {
return ast.value;
}
case Kind.INT:
case Kind.FLOAT: {
return parseFloat(ast.value);
}
case Kind.OBJECT: {
const value = Object.create(null);
ast.fields.forEach(field => {
value[field.name.value] = parseLiteral(field.value);
});

return value;
}
case Kind.LIST: {
return ast.values.map(parseLiteral);
}
default:
return null;
}
}

export function fieldMapToFieldConfigMap(
fields: GraphQLFieldMap<any, any>,
registry: TypeRegistry,
resolveType: ResolveType<any>,
): GraphQLFieldConfigMap<any, any> {
const result: GraphQLFieldConfigMap<any, any> = {};
Object.keys(fields).forEach(name => {
result[name] = fieldToFieldConfig(fields[name], registry);
const field = fields[name];
const type = resolveType(field.type);
if (type !== null) {
result[name] = fieldToFieldConfig(fields[name], resolveType);
}
});
return result;
}

export function createResolveType(
getType: (name: string, type: GraphQLType) => GraphQLType | null,
): ResolveType<any> {
const resolveType = <T extends GraphQLType>(type: T): T => {
if (type instanceof GraphQLList) {
const innerType = resolveType(type.ofType);
if (innerType === null) {
return null;
} else {
return new GraphQLList(innerType) as T;
}
} else if (type instanceof GraphQLNonNull) {
const innerType = resolveType(type.ofType);
if (innerType === null) {
return null;
} else {
return new GraphQLNonNull(innerType) as T;
}
} else if (isNamedType(type)) {
return getType(getNamedType(type).name, type) as T;
} else {
return type;
}
};
return resolveType;
}

function fieldToFieldConfig(
field: GraphQLField<any, any>,
registry: TypeRegistry,
resolveType: ResolveType<any>,
): GraphQLFieldConfig<any, any> {
return {
type: registry.resolveType(field.type),
args: argsToFieldConfigArgumentMap(field.args, registry),
type: resolveType(field.type),
args: argsToFieldConfigArgumentMap(field.args, resolveType),
resolve: defaultMergedResolver,
description: field.description,
deprecationReason: field.deprecationReason,
Expand All @@ -98,24 +205,24 @@ function fieldToFieldConfig(

function argsToFieldConfigArgumentMap(
args: Array<GraphQLArgument>,
registry: TypeRegistry,
resolveType: ResolveType<any>,
): GraphQLFieldConfigArgumentMap {
const result: GraphQLFieldConfigArgumentMap = {};
args.forEach(arg => {
const [name, def] = argumentToArgumentConfig(arg, registry);
const [name, def] = argumentToArgumentConfig(arg, resolveType);
result[name] = def;
});
return result;
}

function argumentToArgumentConfig(
argument: GraphQLArgument,
registry: TypeRegistry,
resolveType: ResolveType<any>,
): [string, GraphQLArgumentConfig] {
return [
argument.name,
{
type: registry.resolveType(argument.type),
type: resolveType(argument.type),
defaultValue: argument.defaultValue,
description: argument.description,
},
Expand All @@ -124,21 +231,25 @@ function argumentToArgumentConfig(

function inputFieldMapToFieldConfigMap(
fields: GraphQLInputFieldMap,
registry: TypeRegistry,
resolveType: ResolveType<any>,
): GraphQLInputFieldConfigMap {
const result: GraphQLInputFieldConfigMap = {};
Object.keys(fields).forEach(name => {
result[name] = inputFieldToFieldConfig(fields[name], registry);
const field = fields[name];
const type = resolveType(field.type);
if (type !== null) {
result[name] = inputFieldToFieldConfig(fields[name], resolveType);
}
});
return result;
}

function inputFieldToFieldConfig(
field: GraphQLInputField,
registry: TypeRegistry,
resolveType: ResolveType<any>,
): GraphQLInputFieldConfig {
return {
type: registry.resolveType(field.type),
type: resolveType(field.type),
defaultValue: field.defaultValue,
description: field.description,
astNode: field.astNode,
Expand Down
Loading