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

Commit

Permalink
feat(mergeTypes): adds abiliity to merge types
Browse files Browse the repository at this point in the history
  • Loading branch information
yaacovCR committed Nov 20, 2019
1 parent 53e3662 commit 563cdce
Show file tree
Hide file tree
Showing 14 changed files with 336 additions and 186 deletions.
15 changes: 12 additions & 3 deletions src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ export type MergedTypeConfig = {

export type MergedTypeResolver = (
subschema: GraphQLSchema | SubschemaConfig,
parent: any,
args: Record<string, any>,
originalResult: any,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
) => any;
Expand Down Expand Up @@ -144,14 +143,19 @@ export type MergeInfo = {
fragment: string;
}>;
replacementFragments: ReplacementFragmentMapping,
mergedTypes: Record<string, Array<SubschemaConfig>>,
mergedTypes: MergedTypeMapping,
delegateToSchema<TContext>(options: IDelegateToSchemaOptions<TContext>): any;
};

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

export type MergedTypeMapping = Record<string, {
fragment: InlineFragmentNode,
subschemas: Array<SubschemaConfig>,
}>;

export type IFieldResolver<TSource, TContext, TArgs = Record<string, any>> = (
source: TSource,
args: TArgs,
Expand Down Expand Up @@ -332,3 +336,8 @@ export type TypeVisitor = (
type: GraphQLType,
schema: GraphQLSchema,
) => GraphQLNamedType | null | undefined;

export type Path = {
prev: Path;
key: string | number;
};
123 changes: 104 additions & 19 deletions src/stitching/checkResultAndHandleErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import {
isLeafType,
isListType,
ExecutionResult,
GraphQLCompositeType,
GraphQLError,
GraphQLType,
GraphQLSchema,
FieldNode,
isAbstractType,
} from 'graphql';
import { getResponseKeyFromInfo } from './getResponseKeyFromInfo';
import {
Expand All @@ -19,10 +22,13 @@ import {
import {
SubschemaConfig,
IGraphQLToolsResolveInfo,
Path,
} from '../Interfaces';
import resolveFromParentTypename from './resolveFromParentTypename';

export function checkResultAndHandleErrors(
result: ExecutionResult,
context: Record<string, any>,
info: GraphQLResolveInfo,
responseKey?: string,
subschema?: GraphQLSchema | SubschemaConfig,
Expand All @@ -32,48 +38,127 @@ export function checkResultAndHandleErrors(
}

if (!result.data || result.data[responseKey] == null) {
return (result.errors) ? handleErrors(info, result.errors) : null;
return (result.errors) ? handleErrors(info.fieldNodes, info.path, result.errors) : null;
}

return handleResult(info, result.data[responseKey], result.errors || [], [subschema]);
return handleResult(
getNullableType(info.returnType),
result.data[responseKey],
result.errors || [],
[subschema],
context,
info,
);
}

export function handleResult(
info: IGraphQLToolsResolveInfo,
type: GraphQLType,
result: any,
errors: ReadonlyArray<GraphQLError>,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
): any {
const nullableType = getNullableType(info.returnType);

if (isLeafType(nullableType)) {
return nullableType.parseValue(result);
} else if (isCompositeType(nullableType)) {
return createMergedResult(result, errors, subschemas);
} else if (isListType(nullableType)) {
return createMergedResult(result, errors).map(
(r: any) => parseOutputValue(getNullableType(nullableType.ofType), r)
if (isLeafType(type)) {
return type.parseValue(result);
} else if (isCompositeType(type)) {
return mergeResultsFromOtherSubschemas(
type,
createMergedResult(result, errors, subschemas),
subschemas,
context,
info,
);
} else if (isListType(type)) {
return createMergedResult(result, errors, subschemas).map(
(r: any) => handleListResult(
getNullableType(type.ofType),
r,
subschemas,
context,
info,
)
);
}
}

function parseOutputValue(type: GraphQLType, value: any) {
function handleListResult(
type: GraphQLType,
result: any,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
) {
if (isLeafType(type)) {
return type.parseValue(value);
return type.parseValue(result);
} else if (isCompositeType(type)) {
return value;
return mergeResultsFromOtherSubschemas(
type,
result,
subschemas,
context,
info
);
} else if (isListType(type)) {
return value.map((v: any) => parseOutputValue(getNullableType(type.ofType), v));
return result.map((r: any) => handleListResult(
getNullableType(type.ofType),
r,
subschemas,
context,
info,
));
}
}

async function mergeResultsFromOtherSubschemas(
type: GraphQLCompositeType,
result: any,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
): Promise<any> {
if (info.mergeInfo) {
let typeName: string;
if (isAbstractType(type)) {
typeName = info.schema.getTypeMap()[resolveFromParentTypename(result)].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 results = await Promise.all(remainingSubschemas.map(subschema => {
const mergedTypeResolver = subschema.mergedTypeConfigs[typeName].mergedTypeResolver;
return mergedTypeResolver(subschema, result, context, {
...info,
mergeInfo: {
...info.mergeInfo,
mergedTypes: {},
},
});
}));
results.forEach((r: ExecutionResult) => Object.assign(result, r));
}
}
}

return result;
}

export function handleErrors(
info: GraphQLResolveInfo,
fieldNodes: ReadonlyArray<FieldNode>,
path: Path,
errors: ReadonlyArray<GraphQLError>,
) {
throw relocatedError(
combineErrors(errors),
info.fieldNodes,
responsePathAsArray(info.path)
fieldNodes,
responsePathAsArray(path)
);
}
17 changes: 7 additions & 10 deletions src/stitching/createMergedResolver.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { GraphQLObjectType, getNamedType, responsePathAsArray } from 'graphql';
import { GraphQLObjectType, getNamedType } from 'graphql';
import { IFieldResolver } from '../Interfaces';
import {
relocatedError,
combineErrors,
getErrorsFromParent,
createMergedResult,
getSubschemasFromParent,
} from './errors';
import defaultMergedResolver from './defaultMergedResolver';
import { extractOneLevelOfFields } from './extractFields';
import { handleErrors } from './checkResultAndHandleErrors';

export function wrapField(wrapper: string, fieldName: string): IFieldResolver<any, any> {
return createMergedResolver({ fromPath: [wrapper, fieldName] });
Expand All @@ -28,7 +28,7 @@ export function createMergedResolver({
toPath?: Array<string>;
fromPath?: Array<string>;
}): IFieldResolver<any, any> {
return (parent, args, context, info) => {
return async (parent, args, context, info) => {

let fieldNodes = info.fieldNodes;
let returnType = info.returnType;
Expand All @@ -55,19 +55,16 @@ export function createMergedResolver({
for (let i = 0; i < fromParentPathLength; i++) {
const responseKey = fromPath[i];
const errors = getErrorsFromParent(parent, responseKey);
const subschemas = getSubschemasFromParent(parent);
const result = parent[responseKey];
if (result == null) {
if (errors.length) {
throw relocatedError(
combineErrors(errors),
fieldNodes,
responsePathAsArray(path)
);
handleErrors(fieldNodes, path, errors);
} else {
return null;
}
}
parent = createMergedResult(result, errors);
parent = createMergedResult(result, errors, subschemas);
}

fieldName = fromPath[fromPathLength - 1];
Expand Down
28 changes: 4 additions & 24 deletions src/stitching/defaultMergedResolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defaultFieldResolver, getNamedType, ExecutionResult } from 'graphql';
import { defaultFieldResolver, getNullableType } from 'graphql';
import { getErrorsFromParent, getSubschemasFromParent, MERGED_NULL_SYMBOL } from './errors';
import { handleResult, handleErrors } from './checkResultAndHandleErrors';
import { getResponseKeyFromInfo } from './getResponseKeyFromInfo';
Expand All @@ -8,7 +8,7 @@ import { IGraphQLToolsResolveInfo } from '../Interfaces';
// a) handle aliases for proxied schemas
// b) handle errors from proxied schemas
// c) handle external to internal enum coversion
export default async function defaultMergedResolver(
export default function defaultMergedResolver(
parent: Record<string, any>,
args: Record<string, any>,
context: Record<string, any>,
Expand All @@ -30,29 +30,9 @@ export default async function defaultMergedResolver(
const result = parent[responseKey];

if (result == null || result[MERGED_NULL_SYMBOL]) {
return (errors.length) ? handleErrors(info, errors) : null;
return (errors.length) ? handleErrors(info.fieldNodes, info.path, errors) : null;
}

const parentSubschemas = getSubschemasFromParent(parent);
const mergedResult = handleResult(info, result, errors, parentSubschemas);
if (info.mergeInfo) {
const typeName = getNamedType(info.returnType).name;
const initialSubschemas = info.mergeInfo.mergedTypes[typeName];
if (initialSubschemas) {
const remainingSubschemas = info.mergeInfo.mergedTypes[typeName].filter(
subschema => !parentSubschemas.includes(subschema)
);
if (remainingSubschemas.length) {
const additionalResults = await Promise.all(remainingSubschemas.map(subschema => {
const mergedTypeResolver = subschema.mergedTypeConfigs[typeName].mergedTypeResolver;
return mergedTypeResolver(subschema, parent, args, context, info);
}));
additionalResults.forEach((additionalResult: ExecutionResult) => {
Object.assign(result, additionalResult);
});
}
}
}

return mergedResult;
return handleResult(getNullableType(info.returnType), result, errors, parentSubschemas, context, info);
}
6 changes: 4 additions & 2 deletions src/stitching/delegateToSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import AddReplacementFragments from '../transforms/AddReplacementFragments';
import { ApolloLink, execute as executeLink } from 'apollo-link';
import linkToFetcher from './linkToFetcher';
import { observableToAsyncIterable } from './observableToAsyncIterable';
import { AddMergedTypeFragments } from '../transforms';

export default function delegateToSchema(
options: IDelegateToSchemaOptions | GraphQLSchema,
Expand Down Expand Up @@ -98,14 +99,15 @@ async function delegateToSchemaImplementation({
};

transforms = [
new CheckResultAndHandleErrors(info, fieldName, subschema),
new CheckResultAndHandleErrors(info, fieldName, subschema, context),
...transforms,
new ExpandAbstractTypes(info.schema, targetSchema),
];

if (info.mergeInfo) {
transforms.push(
new AddReplacementFragments(targetSchema, info.mergeInfo.replacementFragments)
new AddReplacementFragments(targetSchema, info.mergeInfo.replacementFragments),
new AddMergedTypeFragments(targetSchema, info.mergeInfo.mergedTypes),
);
}

Expand Down
26 changes: 12 additions & 14 deletions src/stitching/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,22 @@ export function relocatedError(
}

export function createMergedResult(
object: any,
childrenErrors: ReadonlyArray<GraphQLError> = [],
result: any,
errors: ReadonlyArray<GraphQLError> = [],
subschemas: Array<GraphQLSchema | SubschemaConfig> = [],
): any {
if (object == null) {
object = {
if (result == null) {
result = {
[MERGED_NULL_SYMBOL]: true,
};
} else if (typeof object !== 'object') {
return object;
} else if (typeof result !== 'object') {
return result;
}

if (Array.isArray(object)) {
if (Array.isArray(result)) {
const byIndex = {};

childrenErrors.forEach(error => {
errors.forEach((error: GraphQLError) => {
if (!error.path) {
return;
}
Expand All @@ -80,22 +80,20 @@ export function createMergedResult(
byIndex[index] = current;
});

object = object.map((item, index) => createMergedResult(item, byIndex[index]), subschemas);

return object;
return result.map((item, index) => createMergedResult(item, byIndex[index], subschemas));
}

object[ERROR_SYMBOL] = childrenErrors.map(error => {
result[ERROR_SYMBOL] = errors.map(error => {
const newError = relocatedError(
error,
error.nodes,
error.path ? error.path.slice(1) : undefined
);
return newError;
});
object[SUBSCHEMAS_SYMBOL] = subschemas;
result[SUBSCHEMAS_SYMBOL] = subschemas;

return object;
return result;
}

export function isParentProxiedResult(parent: any) {
Expand Down

0 comments on commit 563cdce

Please sign in to comment.