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

Commit

Permalink
fix(merging): fixes merging for non root types
Browse files Browse the repository at this point in the history
adds a canonical test
  • Loading branch information
yaacovCR committed Feb 2, 2020
1 parent cc0cc91 commit 7093f10
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 23 deletions.
16 changes: 10 additions & 6 deletions src/stitching/checkResultAndHandleErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,20 @@ export function handleObject(
return object;
}

const subFields = collectSubFields(info, object.__typename);

const selections = getFieldsNotInSubschema(
subFields,
subschema,
mergedTypeInfo,
object.__typename,
);

return mergeFields(
mergedTypeInfo,
typeName,
object,
getFieldsNotInSubschema(
collectSubFields(info, object.__typename),
subschema,
mergedTypeInfo,
object.__typename,
),
selections,
[subschema as SubschemaConfig],
targetSubschemas,
context,
Expand Down
25 changes: 11 additions & 14 deletions src/stitching/proxiedResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import { relocatedError } from './errors';
import { mergeDeep } from '../utils';

export let OBJECT_SUBSCHEMA_SYMBOL: any;
export let SUBSCHEMA_MAP_SYMBOL: any;
export let FIELD_SUBSCHEMA_MAP_SYMBOL: any;
export let ERROR_SYMBOL: any;
if (
(typeof global !== 'undefined' && 'Symbol' in global) ||
(typeof window !== 'undefined' && 'Symbol' in window)
) {
OBJECT_SUBSCHEMA_SYMBOL = Symbol('initialSubschema');
SUBSCHEMA_MAP_SYMBOL = Symbol('subschemaMap');
FIELD_SUBSCHEMA_MAP_SYMBOL = Symbol('subschemaMap');
ERROR_SYMBOL = Symbol('subschemaErrors');
} else {
OBJECT_SUBSCHEMA_SYMBOL = Symbol('@@__initialSubschema');
SUBSCHEMA_MAP_SYMBOL = Symbol('@@__subschemaMap');
FIELD_SUBSCHEMA_MAP_SYMBOL = Symbol('@@__subschemaMap');
ERROR_SYMBOL = '@@__subschemaErrors';
}

Expand All @@ -29,19 +29,14 @@ export function isProxiedResult(result: any) {
}

export function getSubschema(result: any, responseKey: string): GraphQLSchema | SubschemaConfig {
const subschema = result[SUBSCHEMA_MAP_SYMBOL] && result[SUBSCHEMA_MAP_SYMBOL][responseKey];
const subschema = result[FIELD_SUBSCHEMA_MAP_SYMBOL] && result[FIELD_SUBSCHEMA_MAP_SYMBOL][responseKey];
return subschema ? subschema : result[OBJECT_SUBSCHEMA_SYMBOL];
}

export function setObjectSubschema(result: any, subschema: GraphQLSchema | SubschemaConfig) {
result[OBJECT_SUBSCHEMA_SYMBOL] = subschema;
}

export function setSubschemaForKey(result: any, responseKey: string, subschema: GraphQLSchema | SubschemaConfig) {
result[SUBSCHEMA_MAP_SYMBOL] = result[SUBSCHEMA_MAP_SYMBOL] || Object.create(null);
result[SUBSCHEMA_MAP_SYMBOL][responseKey] = subschema;
}

export function setErrors(result: any, errors: Array<GraphQLError>) {
result[ERROR_SYMBOL] = errors;
}
Expand Down Expand Up @@ -130,15 +125,17 @@ export function dehoistResult(parent: any, delimeter: string = '__gqltf__'): any

export function mergeProxiedResults(target: any, ...sources: any): any {
const errors = target[ERROR_SYMBOL].concat(sources.map((source: any) => source[ERROR_SYMBOL]));
const subschemaMap = sources.reduce((acc: Record<any, SubschemaConfig>, source: any) => {
const fieldSubschemaMap = sources.reduce((acc: Record<any, SubschemaConfig>, source: any) => {
const subschema = source[OBJECT_SUBSCHEMA_SYMBOL];
Object.keys(source).forEach(key => {
acc[key] = subschema;
});
return acc;
}, {});
return mergeDeep(target, ...sources, {
[ERROR_SYMBOL]: errors,
[SUBSCHEMA_MAP_SYMBOL]: subschemaMap,
});
const result = mergeDeep(target, ...sources);
result[ERROR_SYMBOL] = errors;
result[FIELD_SUBSCHEMA_MAP_SYMBOL] = target[FIELD_SUBSCHEMA_MAP_SYMBOL] ?
mergeDeep(target[FIELD_SUBSCHEMA_MAP_SYMBOL], fieldSubschemaMap) :
fieldSubschemaMap;
return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@
// always returning the necessary object fields:
// https://medium.com/paypal-engineering/graphql-resolvers-best-practices-cd36fdbcef55

// This is achieved at the considerable cost of moving all of the delegation
// logic from the gateway to each subschema so that each subschema imports all
// the required types and performs all delegation.

// The fragment field is still necessary when working with a remote schema
// where this is not possible.

import { expect } from 'chai';
import { graphql } from 'graphql';
import { delegateToSchema, mergeSchemas } from '../index';
import { addMockFunctionsToSchema } from '../mock';
import {
delegateToSchema,
mergeSchemas,
addMockFunctionsToSchema,
} from '../index';

const chirpTypeDefs = `
type Chirp {
Expand Down
110 changes: 110 additions & 0 deletions src/test/testTypeMerging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* tslint:disable:no-unused-expression */

// The below is meant to be an alternative canonical schema stitching example
// which relies on type merging.

import { expect } from 'chai';
import { graphql } from 'graphql';
import {
delegateToSchema,
mergeSchemas,
addMockFunctionsToSchema,
makeExecutableSchema,
} from '../index';

const chirpSchema = makeExecutableSchema({
typeDefs: `
type Chirp {
id: ID!
text: String
author: User
}
type User {
id: ID!
chirps: [Chirp]
}
type Query {
userById(id: ID!): User
}
`,
});

addMockFunctionsToSchema({ schema: chirpSchema });

const authorSchema = makeExecutableSchema({
typeDefs: `
type User {
id: ID!
email: String
}
type Query {
userById(id: ID!): User
}
`,
});

addMockFunctionsToSchema({ schema: authorSchema });

const mergedSchema = mergeSchemas({
subschemas: [{
schema: chirpSchema,
mergedTypeConfigs: {
User: {
selectionSet: '{ id }',
merge: (originalResult, context, info, subschema, selectionSet) => delegateToSchema({
schema: subschema,
operation: 'query',
fieldName: 'userById',
args: { id: originalResult.id },
selectionSet,
context,
info,
skipTypeMerging: true,
}),
}
},
}, {
schema: authorSchema,
mergedTypeConfigs: {
User: {
selectionSet: '{ id }',
merge: (originalResult, context, info, subschema, selectionSet) => delegateToSchema({
schema: subschema,
operation: 'query',
fieldName: 'userById',
args: { id: originalResult.id },
selectionSet,
context,
info,
skipTypeMerging: true,
}),
}
},
}],
});

describe('merging using type merging', () => {
it('works', async () => {
const query = `
query {
userById(id: 5) {
chirps {
id
textAlias: text
author {
email
}
}
}
}
`;

const result = await graphql(mergedSchema, query);

expect(result.errors).to.be.undefined;
expect(result.data.userById.chirps[1].id).to.not.be.null;
expect(result.data.userById.chirps[1].text).to.not.be.null;
expect(result.data.userById.chirps[1].author.email).to.not.be.null;
});
});
3 changes: 2 additions & 1 deletion src/test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import './testResolution';
import './testSchemaGenerator';
import './testTransforms';
import './testExtensionExtraction';
import './testIntegration';
import './testStitchingFromSubschemas';
import './testTypeMerging';
import './testUpload';
import './testUtils';

0 comments on commit 7093f10

Please sign in to comment.