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

Commit

Permalink
feat(stitching): support advanced type merging
Browse files Browse the repository at this point in the history
Also fixes algorithmic error in type merging in which previously not recorded the correct originating subschema for fields. Nested type merging is now enabled, with tests (hopefully!) to follow.
  • Loading branch information
yaacovCR committed Jan 23, 2020
1 parent 3afe7d8 commit 579ed9c
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 137 deletions.
19 changes: 13 additions & 6 deletions src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
GraphQLOutputType,
} from 'graphql';

import { TypeMap } from 'graphql/type/schema';

import { SchemaDirectiveVisitor } from './utils/SchemaDirectiveVisitor';
import { SchemaVisitor } from './utils/SchemaVisitor';

Expand Down Expand Up @@ -91,14 +93,16 @@ export type SubschemaConfig = {

export type MergedTypeConfig = {
fragment?: string;
mergedTypeResolver: MergedTypeResolver;
parsedFragment?: InlineFragmentNode,
merge: MergedTypeResolver;
};

export type MergedTypeResolver = (
subschema: GraphQLSchema | SubschemaConfig,
originalResult: any,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
subschema: GraphQLSchema | SubschemaConfig,
fieldNodes: Array<FieldNode>,
) => any;

export type GraphQLSchemaWithTransforms = GraphQLSchema & { transforms?: Array<Transform> };
Expand Down Expand Up @@ -162,18 +166,21 @@ export type MergeInfo = {
fragment: string;
}>;
replacementFragments: ReplacementFragmentMapping,
mergedTypes: MergedTypeMapping,
mergedTypes: Record<string, MergedTypeInfo>,
delegateToSchema<TContext>(options: IDelegateToSchemaOptions<TContext>): any;
};

export type ReplacementFragmentMapping = {
[typeName: string]: { [fieldName: string]: InlineFragmentNode };
};

export type MergedTypeMapping = Record<string, {
fragment: InlineFragmentNode,
export type MergedTypeInfo = {
subschemas: Array<SubschemaConfig>,
}>;
fragment?: InlineFragmentNode,
uniqueFields: Record<string, SubschemaConfig>,
nonUniqueFields: Record<string, Array<SubschemaConfig>>,
typeMaps: Map<SubschemaConfig, TypeMap>,
};

export type IFieldResolver<TSource, TContext, TArgs = Record<string, any>> = (
source: TSource,
Expand Down
143 changes: 63 additions & 80 deletions src/stitching/checkResultAndHandleErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
GraphQLSchema,
FieldNode,
isAbstractType,
GraphQLObjectType,
} from 'graphql';
import { getResponseKeyFromInfo } from './getResponseKeyFromInfo';
import {
Expand All @@ -24,10 +25,12 @@ import {
import {
SubschemaConfig,
IGraphQLToolsResolveInfo,
isSubschemaConfig,
} from '../Interfaces';
import resolveFromParentTypename from './resolveFromParentTypename';
import { setErrors, setSubschemas } from './proxiedResult';
import { mergeDeep } from '../utils';
import { setErrors, setObjectSubschema } from './proxiedResult';
import { collectFields } from '../utils';
import { mergeFields } from './mergeFields';

export function checkResultAndHandleErrors(
result: ExecutionResult,
Expand All @@ -44,15 +47,14 @@ export function checkResultAndHandleErrors(

const errors = result.errors || [];
const data = result.data && result.data[responseKey];
const subschemas = [subschema];

return handleResult(data, errors, subschemas, context, info, returnType, skipTypeMerging);
return handleResult(data, errors, subschema, context, info, returnType, skipTypeMerging);
}

export function handleResult(
result: any,
errors: ReadonlyArray<GraphQLError>,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
subschema: GraphQLSchema | SubschemaConfig,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
returnType = info.returnType,
Expand All @@ -67,55 +69,17 @@ export function handleResult(
if (isLeafType(type)) {
return type.parseValue(result);
} else if (isCompositeType(type)) {
return handleObject(type, result, errors, subschemas, context, info, skipTypeMerging);
return handleObject(type, result, errors, subschema, context, info, skipTypeMerging);
} else if (isListType(type)) {
return handleList(type, result, errors, subschemas, context, info, skipTypeMerging);
}
}

export function makeObjectProxiedResult(
object: any,
errors: ReadonlyArray<GraphQLError>,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
) {
setErrors(object, errors.map(error => {
return relocatedError(
error,
error.nodes,
error.path ? error.path.slice(1) : undefined
);
}));
setSubschemas(object, subschemas);
}

export function handleObject(
type: GraphQLCompositeType,
object: any,
errors: ReadonlyArray<GraphQLError>,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
skipTypeMerging?: boolean,
) {
makeObjectProxiedResult(object, errors, subschemas);
if (skipTypeMerging || !info.mergeInfo) {
return object;
} else {
return mergeFields(
type,
object,
subschemas,
context,
info,
);
return handleList(type, result, errors, subschema, context, info, skipTypeMerging);
}
}

function handleList(
type: GraphQLList<any>,
list: Array<any>,
errors: ReadonlyArray<GraphQLError>,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
subschema: GraphQLSchema | SubschemaConfig,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
skipTypeMerging?: boolean,
Expand All @@ -127,7 +91,7 @@ function handleList(
listMember,
index,
childErrors[index] || [],
subschemas,
subschema,
context,
info,
skipTypeMerging,
Expand All @@ -141,7 +105,7 @@ function handleListMember(
listMember: any,
index: number,
errors: ReadonlyArray<GraphQLError>,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
subschema: GraphQLSchema | SubschemaConfig,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
skipTypeMerging?: boolean,
Expand All @@ -153,56 +117,75 @@ function handleListMember(
if (isLeafType(type)) {
return type.parseValue(listMember);
} else if (isCompositeType(type)) {
return handleObject(type, listMember, errors, subschemas, context, info, skipTypeMerging);
return handleObject(type, listMember, errors, subschema, context, info, skipTypeMerging);
} else if (isListType(type)) {
return handleList(type, listMember, errors, subschemas, context, info, skipTypeMerging);
return handleList(type, listMember, errors, subschema, context, info, skipTypeMerging);
}
}

function mergeFields(
export function handleObject(
type: GraphQLCompositeType,
object: any,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
errors: ReadonlyArray<GraphQLError>,
subschema: GraphQLSchema | SubschemaConfig,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
): any {
skipTypeMerging?: boolean,
) {
setErrors(object, errors.map(error => {
return relocatedError(
error,
error.nodes,
error.path ? error.path.slice(1) : undefined
);
}));

setObjectSubschema(object, subschema);

if (skipTypeMerging || !info.mergeInfo) {
return object;
}

let typeName: string;
if (isAbstractType(type)) {
typeName = info.schema.getTypeMap()[resolveFromParentTypename(object)].name;
} else {
typeName = type.name;
}

const initialSchemas =
info.mergeInfo.mergedTypes[typeName] &&
info.mergeInfo.mergedTypes[typeName].subschemas;
if (initialSchemas) {
const remainingSubschemas = initialSchemas.filter(
subschema => !subschemas.includes(subschema)
);
if (remainingSubschemas.length) {
const maybePromises = remainingSubschemas.map(subschema => {
return subschema.mergedTypeConfigs[typeName].mergedTypeResolver(subschema, object, context, info);
});
const mergedTypeInfo = info.mergeInfo.mergedTypes[typeName];
let subschemas = mergedTypeInfo && mergedTypeInfo.subschemas;

let containsPromises = false; {
for (const maybePromise of maybePromises) {
if (maybePromise instanceof Promise) {
containsPromises = true;
break;
}
}
}
if (containsPromises) {
return Promise.all(maybePromises).
then(results => results.reduce((acc: any, r: ExecutionResult) => mergeDeep(acc, r), object));
} else {
return maybePromises.reduce((acc: any, r: ExecutionResult) => mergeDeep(acc, r), object);
}
}
if (!subschemas) {
return object;
}

return object;
subschemas = subschemas.filter(s => s !== subschema);
if (!subschemas.length) {
return object;
}

const typeMap = isSubschemaConfig(subschema) ?
mergedTypeInfo.typeMaps.get(subschema) : subschema.getTypeMap();
const fields = (typeMap[typeName] as GraphQLObjectType).getFields();
const selections: Array<FieldNode> = [];
info.fieldNodes.forEach(fieldNode => {
collectFields(fieldNode.selectionSet, info.fragments).forEach(s => {
if (!fields[s.name.value]) {
selections.push(s);
}
});
});

return mergeFields(
mergedTypeInfo,
typeName,
object,
selections,
subschemas,
context,
info,
);
}

export function handleNull(
Expand Down
6 changes: 3 additions & 3 deletions src/stitching/defaultMergedResolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defaultFieldResolver } from 'graphql';
import { getErrors, getSubschemas } from './proxiedResult';
import { getErrors, getSubschema } from './proxiedResult';
import { handleResult } from './checkResultAndHandleErrors';
import { getResponseKeyFromInfo } from './getResponseKeyFromInfo';
import { IGraphQLToolsResolveInfo } from '../Interfaces';
Expand Down Expand Up @@ -28,7 +28,7 @@ export default function defaultMergedResolver(
}

const result = parent[responseKey];
const subschemas = getSubschemas(parent);
const subschema = getSubschema(parent, responseKey);

return handleResult(result, errors, subschemas, context, info);
return handleResult(result, errors, subschema, context, info);
}
Loading

0 comments on commit 579ed9c

Please sign in to comment.