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

enhance(stitching): increase key flexibility when type merging #1888

Merged
merged 12 commits into from
Aug 11, 2020
32 changes: 20 additions & 12 deletions packages/batch-delegate/tests/typeMerging.example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,18 @@ describe('merging using type merging', () => {

const inventorySchema = makeExecutableSchema({
typeDefs: `
input ProductRepresentation {
upc: String!
price: Int
weight: Int
}
type Product {
upc: String!
inStock: Boolean
shippingEstimate: Int
}
type Query {
_productByUpc(
upc: String!,
weight: Int,
price: Int
): Product
_productByRepresentation(product: ProductRepresentation): Product
}
`,
resolvers: {
Expand All @@ -79,10 +80,12 @@ describe('merging using type merging', () => {
}
},
Query: {
_productByUpc: (_root, { upc, ...fields }) => ({
...inventory.find(product => product.upc === upc),
...fields
}),
_productByRepresentation: (_root, { product: { upc, ...fields } }) => {
return {
...inventory.find(product => product.upc === upc),
...fields
};
},
},
},
});
Expand Down Expand Up @@ -230,9 +233,14 @@ describe('merging using type merging', () => {
schema: inventorySchema,
merge: {
Product: {
selectionSet: '{ upc weight price }',
fieldName: '_productByUpc',
args: ({ upc, weight, price }) => ({ upc, weight, price }),
selectionSet: '{ upc }',
fields: {
shippingEstimate: {
selectionSet: '{ price weight }',
},
},
fieldName: '_productByRepresentation',
args: ({ upc, weight, price }) => ({ product: { upc, weight, price } }),
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/delegate/src/delegationBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function defaultDelegationBinding(delegationContext: DelegationContext):
new AddSelectionSets(
info.schema,
returnType,
stitchingInfo.selectionSetsByType,
{},
stitchingInfo.selectionSetsByField,
stitchingInfo.dynamicSelectionSetsByField
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,118 @@
import { GraphQLResolveInfo, FieldNode } from 'graphql';

export function memoizeInfoAnd2Objectsand1Primitive<
export function memoizeInfoAnd2Objects<
T1 extends GraphQLResolveInfo,
T2 extends Record<string, any>,
T3 extends Record<string, any>,
T4 extends string,
R extends any
>(fn: (A1: T1, A2: T2, A3: T3) => R): (A1: T1, A2: T2, A3: T3) => R {
let cache1: WeakMap<ReadonlyArray<FieldNode>, WeakMap<T2, WeakMap<T3, R>>>;

function memoized(a1: T1, a2: T2, a3: T3) {
if (!cache1) {
cache1 = new WeakMap();
const cache2: WeakMap<T2, WeakMap<T3, R>> = new WeakMap();
cache1.set(a1.fieldNodes, cache2);
const cache3: WeakMap<T3, R> = new WeakMap();
cache2.set(a2, cache3);
const newValue = fn(a1, a2, a3);
cache3.set(a3, newValue);
return newValue;
}

let cache2 = cache1.get(a1.fieldNodes);
if (!cache2) {
cache2 = new WeakMap();
cache1.set(a1.fieldNodes, cache2);
const cache3: WeakMap<T3, R> = new WeakMap();
cache2.set(a2, cache3);
const newValue = fn(a1, a2, a3);
cache3.set(a3, newValue);
return newValue;
}

let cache3 = cache2.get(a2);
if (!cache3) {
cache3 = new WeakMap();
cache2.set(a2, cache3);
const newValue = fn(a1, a2, a3);
cache3.set(a3, newValue);
return newValue;
}

const cachedValue = cache3.get(a3);
if (cachedValue === undefined) {
const newValue = fn(a1, a2, a3);
cache3.set(a3, newValue);
return newValue;
}

return cachedValue;
}

return memoized;
}

export function memoize4<
T1 extends Record<string, any>,
T2 extends Record<string, any>,
T3 extends Record<string, any>,
T4 extends Record<string, any>,
R extends any
>(fn: (A1: T1, A2: T2, A3: T3, A4: T4) => R): (A1: T1, A2: T2, A3: T3, A4: T4) => R {
let cache1: WeakMap<ReadonlyArray<FieldNode>, WeakMap<T2, WeakMap<T3, Record<string, R>>>>;
let cache1: WeakMap<T1, WeakMap<T2, WeakMap<T3, WeakMap<T4, R>>>>;

function memoized(a1: T1, a2: T2, a3: T3, a4: T4) {
if (!cache1) {
cache1 = new WeakMap();
const cache2: WeakMap<T2, WeakMap<T3, Record<T4, R>>> = new WeakMap();
cache1.set(a1.fieldNodes, cache2);
const cache3: WeakMap<T3, Record<T4, R>> = new WeakMap();
const cache2: WeakMap<T2, WeakMap<T3, WeakMap<T4, R>>> = new WeakMap();
cache1.set(a1, cache2);
const cache3: WeakMap<T3, WeakMap<T4, R>> = new WeakMap();
cache2.set(a2, cache3);
const cache4 = Object.create(null);
const cache4: WeakMap<T4, R> = new WeakMap();
cache3.set(a3, cache4);
const newValue = fn(a1, a2, a3, a4);
cache4[a4] = newValue;
cache4.set(a4, newValue);
return newValue;
}

let cache2 = cache1.get(a1.fieldNodes);
let cache2 = cache1.get(a1);
if (!cache2) {
cache2 = new WeakMap();
cache1.set(a1.fieldNodes, cache2);
const cache3: WeakMap<T3, Record<T4, R>> = new WeakMap();
cache1.set(a1, cache2);
const cache3: WeakMap<T3, WeakMap<T4, R>> = new WeakMap();
cache2.set(a2, cache3);
const cache4 = Object.create(null);
const cache4: WeakMap<T4, R> = new WeakMap();
cache3.set(a3, cache4);
const newValue = fn(a1, a2, a3, a4);
cache4[a4] = newValue;
cache4.set(a4, newValue);
return newValue;
}

let cache3 = cache2.get(a2);
if (!cache3) {
cache3 = new WeakMap();
cache2.set(a2, cache3);
const cache4 = Object.create(null);
const cache4: WeakMap<T4, R> = new WeakMap();
cache3.set(a3, cache4);
const newValue = fn(a1, a2, a3, a4);
cache4[a4] = newValue;
cache4.set(a4, newValue);
return newValue;
}

let cache4 = cache3.get(a3);
const cache4 = cache3.get(a3);
if (!cache4) {
cache4 = Object.create(null);
const cache4: WeakMap<T4, R> = new WeakMap();
cache3.set(a3, cache4);
const newValue = fn(a1, a2, a3, a4);
cache4[a4] = newValue;
cache4.set(a4, newValue);
return newValue;
}

const cachedValue = cache4[a4];
const cachedValue = cache4.get(a4);
if (cachedValue === undefined) {
const newValue = fn(a1, a2, a3, a4);
cache4[a4] = newValue;
cache4.set(a4, newValue);
return newValue;
}

Expand Down
19 changes: 4 additions & 15 deletions packages/delegate/src/results/getFieldsNotInSubschema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { collectFields, GraphQLExecutionContext } from '@graphql-tools/utils';
import { isSubschemaConfig } from '../Subschema';
import { MergedTypeInfo, SubschemaConfig, StitchingInfo } from '../types';

import { memoizeInfoAnd2Objectsand1Primitive } from './memoize';
import { memoizeInfoAnd2Objects } from '../memoize';

function collectSubFields(info: GraphQLResolveInfo, typeName: string): Record<string, Array<FieldNode>> {
let subFieldNodes: Record<string, Array<FieldNode>> = Object.create(null);
Expand All @@ -28,21 +28,10 @@ function collectSubFields(info: GraphQLResolveInfo, typeName: string): Record<st
});

const stitchingInfo = info.schema.extensions.stitchingInfo as StitchingInfo;
const selectionSetsByType = stitchingInfo.selectionSetsByType;
const selectionSetsByField = stitchingInfo.selectionSetsByField;

Object.keys(subFieldNodes).forEach(responseName => {
const fieldName = subFieldNodes[responseName][0].name.value;
const typeSelectionSet = selectionSetsByType[typeName];
if (typeSelectionSet != null) {
subFieldNodes = collectFields(
partialExecutionContext,
type,
typeSelectionSet,
subFieldNodes,
visitedFragmentNames
);
}
const fieldSelectionSet = selectionSetsByField?.[typeName]?.[fieldName];
if (fieldSelectionSet != null) {
subFieldNodes = collectFields(
Expand All @@ -58,13 +47,13 @@ function collectSubFields(info: GraphQLResolveInfo, typeName: string): Record<st
return subFieldNodes;
}

export const getFieldsNotInSubschema = memoizeInfoAnd2Objectsand1Primitive(function (
export const getFieldsNotInSubschema = memoizeInfoAnd2Objects(function (
info: GraphQLResolveInfo,
subschema: GraphQLSchema | SubschemaConfig,
mergedTypeInfo: MergedTypeInfo,
typeName: string
mergedTypeInfo: MergedTypeInfo
): Array<FieldNode> {
const typeMap = isSubschemaConfig(subschema) ? mergedTypeInfo.typeMaps.get(subschema) : subschema.getTypeMap();
const typeName = mergedTypeInfo.typeName;
const fields = (typeMap[typeName] as GraphQLObjectType).getFields();

const subFieldNodes = collectSubFields(info, typeName);
Expand Down
2 changes: 1 addition & 1 deletion packages/delegate/src/results/handleObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function handleObject(
return object;
}

const fieldNodes = getFieldsNotInSubschema(info, subschema, mergedTypeInfo, typeName);
const fieldNodes = getFieldsNotInSubschema(info, subschema, mergedTypeInfo);

return mergeFields(
mergedTypeInfo,
Expand Down
34 changes: 27 additions & 7 deletions packages/delegate/src/results/mergeFields.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
import { FieldNode, SelectionNode, Kind, GraphQLResolveInfo, SelectionSetNode } from 'graphql';
import { FieldNode, SelectionNode, Kind, GraphQLResolveInfo, SelectionSetNode, GraphQLObjectType } from 'graphql';

import isPromise from 'is-promise';

import { typesContainSelectionSet } from '@graphql-tools/utils';

import { MergedTypeInfo, SubschemaConfig } from '../types';
import { memoize4, memoize3, memoize2 } from '../memoize';

import { memoize3, memoize2 } from './memoize';
import { mergeProxiedResults } from './mergeProxiedResults';

const sortSubschemasByProxiability = memoize3(function (
const sortSubschemasByProxiability = memoize4(function (
mergedTypeInfo: MergedTypeInfo,
sourceSubschemaOrSourceSubschemas: SubschemaConfig | Array<SubschemaConfig>,
targetSubschemas: Array<SubschemaConfig>
targetSubschemas: Array<SubschemaConfig>,
fieldNodes: Array<FieldNode>
): {
proxiableSubschemas: Array<SubschemaConfig>;
nonProxiableSubschemas: Array<SubschemaConfig>;
} {
// 1. calculate if possible to delegate to given subschema
// TODO: change logic so that required selection set can be spread across multiple subschemas?

const proxiableSubschemas: Array<SubschemaConfig> = [];
const nonProxiableSubschemas: Array<SubschemaConfig> = [];

const sourceSubschemas = Array.isArray(sourceSubschemaOrSourceSubschemas)
? sourceSubschemaOrSourceSubschemas
: [sourceSubschemaOrSourceSubschemas];

const typeName = mergedTypeInfo.typeName;
const types = sourceSubschemas.map(sourceSubschema => sourceSubschema.schema.getType(typeName) as GraphQLObjectType);

targetSubschemas.forEach(t => {
if (sourceSubschemas.some(s => mergedTypeInfo.containsSelectionSet.get(s).get(t))) {
const selectionSet = mergedTypeInfo.selectionSets.get(t);
const fieldSelectionSets = mergedTypeInfo.fieldSelectionSets.get(t);
if (!typesContainSelectionSet(types, selectionSet)) {
nonProxiableSubschemas.push(t);
} else if (fieldSelectionSets == null) {
proxiableSubschemas.push(t);
} else if (
fieldNodes.every(fieldNode => {
const fieldName = fieldNode.name.value;
const fieldSelectionSet = fieldSelectionSets[fieldName];
return fieldSelectionSet == null || typesContainSelectionSet(types, fieldSelectionSet);
})
) {
proxiableSubschemas.push(t);
} else {
nonProxiableSubschemas.push(t);
Expand Down Expand Up @@ -140,7 +159,8 @@ export function mergeFields(
const { proxiableSubschemas, nonProxiableSubschemas } = sortSubschemasByProxiability(
mergedTypeInfo,
sourceSubschemaOrSourceSubschemas,
targetSubschemas
targetSubschemas,
fieldNodes
);

const { delegationMap, unproxiableFieldNodes } = buildDelegationPlan(mergedTypeInfo, fieldNodes, proxiableSubschemas);
Expand Down