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

Merge resolver objects in merge schemas #577

Closed
wants to merge 13 commits into from
3 changes: 2 additions & 1 deletion src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {

/* TODO: Add documentation */

export type UnitOrList<Type> = Type | Array<Type>;
export interface IResolverValidationOptions {
requireResolversForArgs?: boolean;
requireResolversForNonScalar?: boolean;
Expand Down Expand Up @@ -71,7 +72,7 @@ export type IConnectors = { [key: string]: IConnector };

export interface IExecutableSchemaDefinition {
typeDefs: ITypeDefinitions;
resolvers?: IResolvers;
resolvers?: IResolvers | Array<IResolvers>;
connectors?: IConnectors;
logger?: ILogger;
allowUndefinedInResolve?: boolean;
Expand Down
12 changes: 10 additions & 2 deletions src/schemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ import {
IConnectorCls,
IResolverValidationOptions,
IDirectiveResolvers,
UnitOrList,
} from './Interfaces';

import { deprecated } from 'deprecated-decorator';
import { mergeDeep } from './stitching/mergeSchemas';

// @schemaDefinition: A GraphQL type schema in shorthand
// @resolvers: Definitions for resolvers to be merged with schema
Expand All @@ -59,7 +61,7 @@ class SchemaError extends Error {
// type definitions can be a string or an array of strings.
function _generateSchema(
typeDefinitions: ITypeDefinitions,
resolveFunctions: IResolvers,
resolveFunctions: UnitOrList<IResolvers>,
logger: ILogger,
// TODO: rename to allowUndefinedInResolve to be consistent
allowUndefinedInResolve: boolean,
Expand All @@ -78,13 +80,19 @@ function _generateSchema(
throw new SchemaError('Must provide resolvers');
}

const resolvers = Array.isArray(resolveFunctions)
? resolveFunctions
.filter(resolverObj => typeof resolverObj === 'object')
.reduce(mergeDeep, {})
: resolveFunctions;

// TODO: check that typeDefinitions is either string or array of strings

const schema = buildSchemaFromTypeDefinitions(typeDefinitions);

addResolveFunctionsToSchema(
schema,
resolveFunctions,
resolvers,
resolverValidationOptions,
);

Expand Down
12 changes: 9 additions & 3 deletions src/stitching/mergeSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
parse,
} from 'graphql';
import TypeRegistry from './TypeRegistry';
import { IResolvers, MergeInfo, IFieldResolver } from '../Interfaces';
import { IResolvers, MergeInfo, IFieldResolver, UnitOrList } from '../Interfaces';
import isEmptyObject from '../isEmptyObject';
import {
extractExtensionDefinitions,
Expand All @@ -42,7 +42,7 @@ export default function mergeSchemas({
left: GraphQLNamedType,
right: GraphQLNamedType,
) => GraphQLNamedType;
resolvers?: IResolvers | ((mergeInfo: MergeInfo) => IResolvers);
resolvers?: UnitOrList<IResolvers | ((mergeInfo: MergeInfo) => IResolvers)>;
}): GraphQLSchema {
if (!onTypeConflict) {
onTypeConflict = defaultOnTypeConflict;
Expand Down Expand Up @@ -177,6 +177,12 @@ export default function mergeSchemas({
if (resolvers) {
if (typeof resolvers === 'function') {
passedResolvers = resolvers(mergeInfo);
} else if (Array.isArray(resolvers)) {
passedResolvers = resolvers
.map(resolver => typeof resolver === 'function'
? resolver(mergeInfo)
: resolver)
.reduce(mergeDeep, {});
} else {
passedResolvers = { ...resolvers };
}
Expand Down Expand Up @@ -303,7 +309,7 @@ function isObject(item: any): Boolean {
return item && typeof item === 'object' && !Array.isArray(item);
}

function mergeDeep(target: any, source: any): any {
export function mergeDeep(target: any, source: any): any {
let output = Object.assign({}, target);
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => {
Expand Down
196 changes: 196 additions & 0 deletions src/test/testMergeSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from './testingSchemas';
import { forAwaitEach } from 'iterall';
import { makeExecutableSchema } from '../schemaGenerator';
import { IResolvers } from '../Interfaces';

const testCombinations = [
{
Expand Down Expand Up @@ -924,6 +925,201 @@ bookingById(id: "b1") {
const Booking = mergedSchema.getType('Booking') as GraphQLObjectType;
expect(Booking.isTypeOf).to.equal(undefined);
});

it('should merge resolvers when passed an array of resolver objects', async () => {
const Scalars = () => ({
TestScalar: new GraphQLScalarType({
name: 'TestScalar',
description: undefined,
serialize: value => value,
parseValue: value => value,
parseLiteral: () => null,
})
});
const Enums = () => ({
NumericEnum: {
TEST: 1
},
Color: {
RED: '#EA3232',
}
});
const PropertyResolvers: IResolvers = {
Property: {
bookings: {
fragment: 'fragment PropertyFragment on Property { id }',
resolve(parent, args, context, info) {
return info.mergeInfo.delegate(
'query',
'bookingsByPropertyId',
{
propertyId: parent.id,
limit: args.limit ? args.limit : null,
},
context,
info,
);
}
}
}
};
const LinkResolvers: (info: any) => IResolvers = (info) => ({
Booking: {
property: {
fragment: 'fragment BookingFragment on Booking { propertyId }',
resolve(parent, args, context) {
return info.mergeInfo.delegate(
'query',
'propertyById',
{
id: parent.propertyId,
},
context,
info
);
}
}
}
});
const Query1 = () => ({
Query: {
color() {
return '#EA3232';
},
numericEnum() {
return 1;
}
}
});
const Query2: (info: any) => IResolvers = () => ({
Query: {
delegateInterfaceTest(parent, args, context, info) {
return info.mergeInfo.delegate(
'query',
'interfaceTest',
{
kind: 'ONE',
},
context,
info,
);
},
delegateArgumentTest(parent, args, context, info) {
return info.mergeInfo.delegate(
'query',
'propertyById',
{
id: 'p1',
},
context,
info,
);
},
linkTest() {
return {
test: 'test',
};
},
node: {
// fragment doesn't work
fragment: 'fragment NodeFragment on Node { id }',
resolve(parent, args, context, info) {
if (args.id.startsWith('p')) {
return info.mergeInfo.delegate(
'query',
'propertyById',
args,
context,
info,
);
} else if (args.id.startsWith('b')) {
return info.mergeInfo.delegate(
'query',
'bookingById',
args,
context,
info,
);
} else if (args.id.startsWith('c')) {
return info.mergeInfo.delegate(
'query',
'customerById',
args,
context,
info,
);
} else {
throw new Error('invalid id');
}
}
}
}
});

const AsyncQuery: (info: any) => IResolvers = (info) => ({
Query: {
async nodes(parent, args, context) {
const bookings = await info.mergeInfo.delegate(
'query',
'bookings',
{},
context,
info,
);
const properties = await info.mergeInfo.delegate(
'query',
'properties',
{},
context,
info,
);
return [...bookings, ...properties];
}
}
});
const schema = mergeSchemas({
schemas: [
propertySchema,
bookingSchema,
productSchema,
scalarTest,
enumTest,
linkSchema,
loneExtend,
localSubscriptionSchema,
],
resolvers: [
Scalars,
Enums,
PropertyResolvers,
LinkResolvers,
Query1,
Query2,
AsyncQuery
]
});

const mergedResult = await graphql(
schema,
`
query {
dateTimeTest
test1: jsonTest(input: { foo: "bar" })
test2: jsonTest(input: 5)
test3: jsonTest(input: "6")
}
`,
);
const expected = {
data: {
dateTimeTest: '1987-09-25T12:00:00',
test1: { foo: 'bar' },
test2: 5,
test3: '6'
}
};
expect(mergedResult).to.deep.equal(expected);
});
});

describe('fragments', () => {
Expand Down
74 changes: 74 additions & 0 deletions src/test/testSchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,80 @@ describe('generating schema from shorthand', () => {
);
});

it('can generate a schema with an array of resolvers', () => {
const shorthand = `
type BirdSpecies {
name: String!,
wingspan: Int
}
type RootQuery {
numberOfSpecies: Int
species(name: String!): [BirdSpecies]
}
schema {
query: RootQuery
}
extend type BirdSpecies {
height: Float
}
`;

const resolveFunctions = {
RootQuery: {
species: (root: any, { name }: { name: string }) => [
{
name: `Hello ${name}!`,
wingspan: 200,
height: 30.2,
},
],
},
};

const otherResolveFunctions = {
BirdSpecies: {
name: (bird: Bird) => bird.name,
wingspan: (bird: Bird) => bird.wingspan,
height: (bird: Bird & { height: number }) => bird.height,
},
RootQuery: {
numberOfSpecies() {
return 1;
}
}
};

const testQuery = `{
numberOfSpecies
species(name: "BigBird"){
name
wingspan
height
}
}`;

const solution = {
data: {
numberOfSpecies: 1,
species: [
{
name: 'Hello BigBird!',
wingspan: 200,
height: 30.2,
},
],
},
};
const jsSchema = makeExecutableSchema({
typeDefs: shorthand,
resolvers: [resolveFunctions, otherResolveFunctions],
});
const resultPromise = graphql(jsSchema, testQuery);
return resultPromise.then(result =>
assert.deepEqual(result, solution as ExecutionResult),
);
});

describe('scalar types', () => {
it('supports passing a GraphQLScalarType in resolveFunctions', () => {
// Here GraphQLJSON is used as an example of non-default GraphQLScalarType
Expand Down