diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 364b48b86cf..c382fef3e92 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -24,6 +24,8 @@ import { GraphQLOutputType, } from 'graphql'; +import { TypeMap } from 'graphql/type/schema'; + import { SchemaDirectiveVisitor } from './utils/SchemaDirectiveVisitor'; import { SchemaVisitor } from './utils/SchemaVisitor'; @@ -100,14 +102,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, info: IGraphQLToolsResolveInfo, + subschema: GraphQLSchema | SubschemaConfig, + fieldNodes: Array, ) => any; export type GraphQLSchemaWithTransforms = GraphQLSchema & { transforms?: Array }; @@ -171,7 +175,7 @@ export type MergeInfo = { fragment: string; }>; replacementFragments: ReplacementFragmentMapping, - mergedTypes: MergedTypeMapping, + mergedTypes: Record, delegateToSchema(options: IDelegateToSchemaOptions): any; }; @@ -179,10 +183,13 @@ export type ReplacementFragmentMapping = { [typeName: string]: { [fieldName: string]: InlineFragmentNode }; }; -export type MergedTypeMapping = Record, -}>; + fragment?: InlineFragmentNode, + uniqueFields: Record, + nonUniqueFields: Record>, + typeMaps: Map, +}; export type IFieldResolver> = ( source: TSource, diff --git a/src/stitching/checkResultAndHandleErrors.ts b/src/stitching/checkResultAndHandleErrors.ts index ecf6b3a3681..3b1bf7e8561 100644 --- a/src/stitching/checkResultAndHandleErrors.ts +++ b/src/stitching/checkResultAndHandleErrors.ts @@ -14,6 +14,7 @@ import { GraphQLSchema, FieldNode, isAbstractType, + GraphQLObjectType, } from 'graphql'; import { getResponseKeyFromInfo } from './getResponseKeyFromInfo'; import { @@ -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, @@ -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, - subschemas: Array, + subschema: GraphQLSchema | SubschemaConfig, context: Record, info: IGraphQLToolsResolveInfo, returnType = info.returnType, @@ -67,47 +69,9 @@ 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, - subschemas: Array, -) { - 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, - subschemas: Array, - context: Record, - 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); } } @@ -115,7 +79,7 @@ function handleList( type: GraphQLList, list: Array, errors: ReadonlyArray, - subschemas: Array, + subschema: GraphQLSchema | SubschemaConfig, context: Record, info: IGraphQLToolsResolveInfo, skipTypeMerging?: boolean, @@ -127,7 +91,7 @@ function handleList( listMember, index, childErrors[index] || [], - subschemas, + subschema, context, info, skipTypeMerging, @@ -141,7 +105,7 @@ function handleListMember( listMember: any, index: number, errors: ReadonlyArray, - subschemas: Array, + subschema: GraphQLSchema | SubschemaConfig, context: Record, info: IGraphQLToolsResolveInfo, skipTypeMerging?: boolean, @@ -153,19 +117,35 @@ 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, + errors: ReadonlyArray, + subschema: GraphQLSchema | SubschemaConfig, context: Record, 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; @@ -173,36 +153,39 @@ function mergeFields( 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 = []; + 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( diff --git a/src/stitching/defaultMergedResolver.ts b/src/stitching/defaultMergedResolver.ts index bec11fd249b..5008ab4c07c 100644 --- a/src/stitching/defaultMergedResolver.ts +++ b/src/stitching/defaultMergedResolver.ts @@ -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'; @@ -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); } diff --git a/src/stitching/mergeFields.ts b/src/stitching/mergeFields.ts new file mode 100644 index 00000000000..4981a1a3cc8 --- /dev/null +++ b/src/stitching/mergeFields.ts @@ -0,0 +1,135 @@ +import { + FieldNode, + SelectionNode, + Kind, +} from 'graphql'; +import { + SubschemaConfig, + IGraphQLToolsResolveInfo, + MergedTypeInfo, +} from '../Interfaces'; +import { mergeProxiedResults } from './proxiedResult'; +import { objectContainsInlineFragment } from '../utils'; + +export function mergeFields( + mergedType: MergedTypeInfo, + typeName: string, + object: any, + originalSelections: Array, + subschemas: Array, + context: Record, + info: IGraphQLToolsResolveInfo, +): any { + // 1. use fragment contained within the map and existing result to calculate + // if possible to delegate to given subschema + + const proxiableSubschemas: Array = []; + const nonProxiableSubschemas: Array = []; + subschemas.forEach(s => { + if (objectContainsInlineFragment(object, s.mergedTypeConfigs[typeName].parsedFragment)) { + proxiableSubschemas.push(s); + } else { + nonProxiableSubschemas.push(s); + } + }); + + // 3. use uniqueFields map to assign fields to subschema if one of possible subschemas + + const uniqueFields = mergedType.uniqueFields; + const nonUniqueFields = mergedType.nonUniqueFields; + const remainingSelections: Array = []; + + const delegationMap: Map> = new Map(); + originalSelections.forEach(selection => { + const uniqueSubschema: SubschemaConfig = uniqueFields[selection.name.value]; + if (uniqueSubschema && proxiableSubschemas.includes(uniqueSubschema)) { + const selections = delegationMap.get(uniqueSubschema); + if (selections) { + selections.push(selection); + } else { + delegationMap.set(uniqueSubschema, [selection]); + } + } else { + remainingSelections.push(selection); + } + }); + + // 4. use nonUniqueFields to assign to a possible subschema, + // preferring one of the subschemas already targets of delegation + + const unproxiableSelections: Array = []; + remainingSelections.forEach(selection => { + let nonUniqueSubschemas: Array = nonUniqueFields[selection.name.value]; + nonUniqueSubschemas = nonUniqueSubschemas.filter(s => proxiableSubschemas.includes(s)); + if (nonUniqueSubschemas) { + const existingSelections = nonUniqueSubschemas.map(s => delegationMap.get(s)).find(selections => selections); + if (existingSelections) { + existingSelections.push(selection); + } else { + delegationMap.set(nonUniqueSubschemas[0], [selection]); + } + } else { + unproxiableSelections.push(selection); + } + }); + + // 5. terminate if no delegations actually possible. + + if (!delegationMap.size) { + return object; + } + + // 6. delegate! + + const maybePromises: Promise | any = []; + delegationMap.forEach((selections: Array, s: SubschemaConfig) => { + const newFieldNodes = [{ + ...info.fieldNodes[0], + selectionSet: { + kind: Kind.SELECTION_SET, + selections, + } + }]; + const maybePromise = s.mergedTypeConfigs[typeName].merge( + object, + context, + info, + s, + newFieldNodes, + ); + maybePromises.push(maybePromise); + }); + + let containsPromises = false; + for (const maybePromise of maybePromises) { + if (maybePromise instanceof Promise) { + containsPromises = true; + break; + } + } + + // 7. repeat if necessary + + const mergeRemaining = (o: any) => { + if (unproxiableSelections) { + return mergeFields( + mergedType, + typeName, + o, + unproxiableSelections, + nonProxiableSubschemas, + context, + info + ); + } else { + return o; + } + }; + + if (containsPromises) { + return Promise.all(maybePromises). + then(results => mergeRemaining(mergeProxiedResults(object, ...results))); + } else { + return mergeRemaining(mergeProxiedResults(object, ...maybePromises)); + } +} diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index 20242d510a2..14963b395f8 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -10,6 +10,7 @@ import { parse, Kind, GraphQLDirective, + InlineFragmentNode, } from 'graphql'; import { IDelegateToSchemaOptions, @@ -21,7 +22,6 @@ import { IResolvers, SubschemaConfig, IGraphQLToolsResolveInfo, - MergedTypeMapping, } from '../Interfaces'; import { extractExtensionDefinitions, @@ -50,6 +50,7 @@ type MergeTypeCandidate = { type: GraphQLNamedType; schema?: GraphQLSchema; subschema?: GraphQLSchema | SubschemaConfig; + transformedSubschema?: GraphQLSchema; }; type CandidateSelector = ( @@ -110,6 +111,7 @@ export default function mergeSchemas({ schema, type: operationTypes[typeName], subschema: schemaLikeObject, + transformedSubschema: schema, }); } }); @@ -135,6 +137,7 @@ export default function mergeSchemas({ schema, type, subschema: schemaLikeObject, + transformedSubschema: schema, }); } }); @@ -271,27 +274,64 @@ function createMergeInfo( allSchemas: Array, typeCandidates: { [name: string]: Array }, ): MergeInfo { - const mergedTypes: MergedTypeMapping = {}; + const mergedTypes = {}; Object.keys(typeCandidates).forEach(typeName => { - const subschemaConfigs: Array = + const mergedTypeCandidates = typeCandidates[typeName] - .filter(typeCandidate => typeCandidate.subschema && isSubschemaConfig(typeCandidate.subschema)) - .map(typeCandidate => typeCandidate.subschema as SubschemaConfig); - - const mergeTypeConfigs = subschemaConfigs - .filter(subschemaConfig => - subschemaConfig.mergedTypeConfigs && subschemaConfig.mergedTypeConfigs[typeName]) - .map(subschemaConfig => subschemaConfig.mergedTypeConfigs[typeName]); - - if (mergeTypeConfigs.length) { - const inlineFragments = mergeTypeConfigs - .filter(mergeTypeConfig => mergeTypeConfig.fragment) - .map(mergeTypeConfig => parseFragmentToInlineFragment(mergeTypeConfig.fragment)); + .filter(typeCandidate => + typeCandidate.subschema && + isSubschemaConfig(typeCandidate.subschema) && + typeCandidate.subschema.mergedTypeConfigs && + typeCandidate.subschema.mergedTypeConfigs[typeName] + ); + + if (mergedTypeCandidates.length) { + const subschemas: Array = []; + const parsedFragments: Array = []; + const fields = Object.create({}); + const typeMaps = new Map(); + + mergedTypeCandidates.forEach(typeCandidate => { + const subschemaConfig = typeCandidate.subschema as SubschemaConfig; + const transformedSubschema = typeCandidate.transformedSubschema; + typeMaps.set(subschemaConfig, transformedSubschema.getTypeMap()); + const type = transformedSubschema.getType(typeName) as GraphQLObjectType; + const fieldMap = type.getFields(); + Object.keys(fieldMap).forEach(fieldName => { + fields[fieldName] = fields[fieldName] || []; + fields[fieldName].push(subschemaConfig); + }); + + const mergedTypeConfig = subschemaConfig.mergedTypeConfigs[typeName]; + if (mergedTypeConfig.fragment) { + const parsedFragment = parseFragmentToInlineFragment(mergedTypeConfig.fragment); + parsedFragments.push(parsedFragment); + mergedTypeConfig.parsedFragment = parsedFragment; + } + + subschemas.push(subschemaConfig); + }); + mergedTypes[typeName] = { - fragment: concatInlineFragments(typeName, inlineFragments), - subschemas: subschemaConfigs, + subschemas, + typeMaps, + uniqueFields: Object.create({}), + nonUniqueFields: Object.create({}), }; + + Object.keys(fields).forEach(fieldName => { + const supportedBySubschemas = fields[fieldName]; + if (supportedBySubschemas.length === 1) { + mergedTypes[typeName].uniqueFields[fieldName] = supportedBySubschemas[0]; + } else { + mergedTypes[typeName].nonUniqueFields[fieldName] = supportedBySubschemas; + } + }); + + if (parsedFragments.length) { + mergedTypes[typeName].fragment = concatInlineFragments(typeName, parsedFragments); + } } }); @@ -367,7 +407,7 @@ function completeMergeInfo( mapping[actualTypeName][field].push(parsedFragment); }); - const replacementFragments = Object.create({}); + const replacementFragments = Object.create(null); Object.keys(mapping).forEach(typeName => { Object.keys(mapping[typeName]).forEach(field => { replacementFragments[typeName] = mapping[typeName] || {}; diff --git a/src/stitching/proxiedResult.ts b/src/stitching/proxiedResult.ts index d5b9bf9dec8..085f6a2cdb5 100644 --- a/src/stitching/proxiedResult.ts +++ b/src/stitching/proxiedResult.ts @@ -4,19 +4,23 @@ import { responsePathAsArray, } from 'graphql'; import { SubschemaConfig, IGraphQLToolsResolveInfo } from '../Interfaces'; -import { handleNull, makeObjectProxiedResult } from './checkResultAndHandleErrors'; +import { handleNull } from './checkResultAndHandleErrors'; import { relocatedError } from './errors'; +import { mergeDeep } from '../utils'; -export let SUBSCHEMAS_SYMBOL: any; +export let OBJECT_SUBSCHEMA_SYMBOL: any; +export let SUBSCHEMA_MAP_SYMBOL: any; export let ERROR_SYMBOL: any; if ( (typeof global !== 'undefined' && 'Symbol' in global) || (typeof window !== 'undefined' && 'Symbol' in window) ) { - SUBSCHEMAS_SYMBOL = Symbol('subschemas'); + OBJECT_SUBSCHEMA_SYMBOL = Symbol('initialSubschema'); + SUBSCHEMA_MAP_SYMBOL = Symbol('subschemaMap'); ERROR_SYMBOL = Symbol('subschemaErrors'); } else { - SUBSCHEMAS_SYMBOL = Symbol('subschemas'); + OBJECT_SUBSCHEMA_SYMBOL = Symbol('@@__initialSubschema'); + SUBSCHEMA_MAP_SYMBOL = Symbol('@@__subschemaMap'); ERROR_SYMBOL = '@@__subschemaErrors'; } @@ -24,12 +28,18 @@ export function isProxiedResult(result: any) { return result && result[ERROR_SYMBOL]; } -export function getSubschemas(result: any): Array { - return result && result[SUBSCHEMAS_SYMBOL]; +export function getSubschema(result: any, responseKey: string): GraphQLSchema | SubschemaConfig { + const subschema = result[SUBSCHEMA_MAP_SYMBOL] && result[SUBSCHEMA_MAP_SYMBOL][responseKey]; + return subschema ? subschema : result[OBJECT_SUBSCHEMA_SYMBOL]; } -export function setSubschemas(result: any, subschemas: Array) { - result[SUBSCHEMAS_SYMBOL] = subschemas; +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) { @@ -66,13 +76,22 @@ export function unwrapResult( for (let i = 0; i < pathLength; i++) { const responseKey = path[i]; const errors = getErrors(parent, responseKey); - const subschemas = getSubschemas(parent); + const subschema = getSubschema(parent, responseKey); const object = parent[responseKey]; if (object == null) { return handleNull(info.fieldNodes, responsePathAsArray(info.path), errors); } - makeObjectProxiedResult(object, errors, subschemas); + + setErrors(object, errors.map(error => { + return relocatedError( + error, + error.nodes, + error.path ? error.path.slice(1) : undefined + ); + })); + setObjectSubschema(object, subschema); + parent = object; } @@ -104,7 +123,22 @@ export function dehoistResult(parent: any, delimeter: string = '__gqltf__'): any } }); - result[SUBSCHEMAS_SYMBOL] = parent[SUBSCHEMAS_SYMBOL]; + result[OBJECT_SUBSCHEMA_SYMBOL] = parent[OBJECT_SUBSCHEMA_SYMBOL]; return result; } + +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, 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, + }); +} diff --git a/src/test/testAlternateMergeSchemas.ts b/src/test/testAlternateMergeSchemas.ts index e51232528cc..1fb8ed56201 100644 --- a/src/test/testAlternateMergeSchemas.ts +++ b/src/test/testAlternateMergeSchemas.ts @@ -1753,11 +1753,12 @@ describe('mergeTypes', () => { mergedTypeConfigs: { Test: { fragment: 'fragment TestFragment on Test { id }', - mergedTypeResolver: (subschema, originalResult, context, info) => delegateToSchema({ + merge: (originalResult, context, info, subschema, fieldNodes) => delegateToSchema({ schema: subschema, operation: 'query', fieldName: 'getTest', args: { id: originalResult.id }, + fieldNodes, context, info, skipTypeMerging: true, @@ -1771,11 +1772,12 @@ describe('mergeTypes', () => { mergedTypeConfigs: { Test: { fragment: 'fragment TestFragment on Test { id }', - mergedTypeResolver: (subschema, originalResult, context, info) => delegateToSchema({ + merge: (originalResult, context, info, subschema, fieldNodes) => delegateToSchema({ schema: subschema, operation: 'query', fieldName: 'getTest', args: { id: originalResult.id }, + fieldNodes, context, info, skipTypeMerging: true, diff --git a/src/transforms/AddMergedTypeFragments.ts b/src/transforms/AddMergedTypeFragments.ts index 6ef732145db..98021a86f4a 100644 --- a/src/transforms/AddMergedTypeFragments.ts +++ b/src/transforms/AddMergedTypeFragments.ts @@ -8,16 +8,16 @@ import { visit, visitWithTypeInfo, } from 'graphql'; -import { Request, MergedTypeMapping } from '../Interfaces'; +import { Request, MergedTypeInfo } from '../Interfaces'; import { Transform } from './transforms'; export default class AddMergedTypeFragments implements Transform { private targetSchema: GraphQLSchema; - private mapping: MergedTypeMapping; + private mapping: Record; constructor( targetSchema: GraphQLSchema, - mapping: MergedTypeMapping, + mapping: Record, ) { this.targetSchema = targetSchema; this.mapping = mapping; @@ -39,7 +39,7 @@ export default class AddMergedTypeFragments implements Transform { function addMergedTypeFragments( targetSchema: GraphQLSchema, document: DocumentNode, - mapping: MergedTypeMapping, + mapping: Record, ): DocumentNode { const typeInfo = new TypeInfo(targetSchema); return visit( diff --git a/src/utils/fragments.ts b/src/utils/fragments.ts index 303374b7ebf..ed4bfc5cfbc 100644 --- a/src/utils/fragments.ts +++ b/src/utils/fragments.ts @@ -132,3 +132,26 @@ export function parseFragmentToInlineFragment( throw new Error('Could not parse fragment'); } + +export function objectContainsInlineFragment(object: any, fragment: InlineFragmentNode): boolean { + for (const selection of fragment.selectionSet.selections) { + if (selection.kind === Kind.FIELD) { + if (selection.alias) { + if (!object[selection.alias.value]) { + return false; + } + } else { + if (!object[selection.name.value]) { + return false; + } + } + } else if (selection.kind === Kind.INLINE_FRAGMENT) { + const containsFragment = objectContainsInlineFragment(object, selection); + if (!containsFragment) { + return false; + } + } + } + + return true; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 1fe3920c316..712a9d0205b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,7 +12,11 @@ export { parseInputValueLiteral, serializeInputValue, } from './transformInputValue'; -export { concatInlineFragments, parseFragmentToInlineFragment } from './fragments'; +export { + concatInlineFragments, + parseFragmentToInlineFragment, + objectContainsInlineFragment, +} from './fragments'; export { mergeDeep } from './mergeDeep'; export { collectFields, diff --git a/src/utils/mergeDeep.ts b/src/utils/mergeDeep.ts index 37ae2782a46..352ba57d818 100644 --- a/src/utils/mergeDeep.ts +++ b/src/utils/mergeDeep.ts @@ -1,18 +1,20 @@ -export function mergeDeep(target: any, source: any): any { +export function mergeDeep(target: any, ...sources: any): any { let output = Object.assign({}, target); - if (isObject(target) && isObject(source)) { - Object.keys(source).forEach(key => { - if (isObject(source[key])) { - if (!(key in target)) { - Object.assign(output, { [key]: source[key] }); + sources.forEach((source: any) => { + if (isObject(target) && isObject(source)) { + Object.keys(source).forEach(key => { + if (isObject(source[key])) { + if (!(key in target)) { + Object.assign(output, { [key]: source[key] }); + } else { + output[key] = mergeDeep(target[key], source[key]); + } } else { - output[key] = mergeDeep(target[key], source[key]); + Object.assign(output, { [key]: source[key] }); } - } else { - Object.assign(output, { [key]: source[key] }); - } - }); - } + }); + } + }); return output; }