Skip to content

Commit 6877b91

Browse files
authoredJul 13, 2021
mergeDeep improvements (#3201)
* enhance(delegate): use Object.assign instead of mergeDeep in mergeExternalObjects * Avoid isScalarType in mergeDeep * No need for isScalarType
1 parent bcdbba3 commit 6877b91

File tree

7 files changed

+63
-55
lines changed

7 files changed

+63
-55
lines changed
 

‎.changeset/green-rocks-swim.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@graphql-tools/utils': major
3+
---
4+
5+
BREAKING CHANGES;
6+
7+
`mergeDeep` now takes an array of sources instead of set of parameters as input and it takes an additional flag to enable prototype merging
8+
Instead of `mergeDeep(...sources)` => `mergeDeep(sources)`

‎packages/delegate/src/externalObjects.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GraphQLSchema, GraphQLError, GraphQLObjectType, SelectionSetNode, locatedError } from 'graphql';
22

3-
import { mergeDeep, relocatedError, GraphQLExecutionContext, collectFields } from '@graphql-tools/utils';
3+
import { relocatedError, GraphQLExecutionContext, collectFields } from '@graphql-tools/utils';
44

55
import { SubschemaConfig, ExternalObject } from './types';
66
import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols';
@@ -73,16 +73,17 @@ export function mergeExternalObjects(
7373
}
7474
}
7575

76-
const combinedResult: ExternalObject = results.reduce(mergeDeep, target);
76+
const combinedResult: ExternalObject = Object.assign({}, target, ...results);
7777

78-
const newFieldSubschemaMap = results.reduce((newFieldSubschemaMap, source) => {
78+
const newFieldSubschemaMap = target[FIELD_SUBSCHEMA_MAP_SYMBOL] ?? Object.create(null);
79+
80+
for (const source of results) {
7981
const objectSubschema = source[OBJECT_SUBSCHEMA_SYMBOL];
8082
const fieldSubschemaMap = source[FIELD_SUBSCHEMA_MAP_SYMBOL];
8183
for (const responseKey in source) {
8284
newFieldSubschemaMap[responseKey] = fieldSubschemaMap?.[responseKey] ?? objectSubschema;
8385
}
84-
return newFieldSubschemaMap;
85-
}, target[FIELD_SUBSCHEMA_MAP_SYMBOL] ?? Object.create(null));
86+
}
8687

8788
combinedResult[FIELD_SUBSCHEMA_MAP_SYMBOL] = newFieldSubschemaMap;
8889
combinedResult[OBJECT_SUBSCHEMA_SYMBOL] = target[OBJECT_SUBSCHEMA_SYMBOL];

‎packages/merge/src/extensions.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,7 @@ export function travelSchemaPossibleExtensions(
140140
}
141141

142142
export function mergeExtensions(extensions: SchemaExtensions[]): SchemaExtensions {
143-
return extensions.reduce(
144-
(result, extensionObj) => [result, extensionObj].reduce<SchemaExtensions>(mergeDeep, {} as SchemaExtensions),
145-
{} as SchemaExtensions
146-
);
143+
return mergeDeep(extensions);
147144
}
148145

149146
function applyExtensionObject(
@@ -154,7 +151,7 @@ function applyExtensionObject(
154151
return;
155152
}
156153

157-
obj.extensions = [obj.extensions || {}, extensions || {}].reduce(mergeDeep, {});
154+
obj.extensions = mergeDeep([obj.extensions || {}, extensions || {}]);
158155
}
159156

160157
export function applyExtensions(schema: GraphQLSchema, extensions: SchemaExtensions): GraphQLSchema {

‎packages/merge/src/merge-resolvers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function mergeResolvers<TSource, TContext>(
6262
resolvers.push(resolversDefinition);
6363
}
6464
}
65-
const result = resolvers.reduce(mergeDeep, {});
65+
const result = mergeDeep(resolvers, true);
6666

6767
if (options?.exclusions) {
6868
for (const exclusion of options.exclusions) {

‎packages/stitching-directives/src/stitchingDirectivesTransformer.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -329,10 +329,10 @@ export function stitchingDirectivesTransformer(
329329

330330
const additionalArgs = mergeDirective['additionalArgs'];
331331
if (additionalArgs != null) {
332-
parsedMergeArgsExpr.args = mergeDeep(
332+
parsedMergeArgsExpr.args = mergeDeep([
333333
parsedMergeArgsExpr.args,
334-
valueFromASTUntyped(parseValue(`{ ${additionalArgs} }`, { noLocation: true }))
335-
);
334+
valueFromASTUntyped(parseValue(`{ ${additionalArgs} }`, { noLocation: true })),
335+
]);
336336
}
337337

338338
mergedTypesResolversInfo[typeName] = {
@@ -473,13 +473,13 @@ function generateArgsFromKeysFn(
473473
): (keys: ReadonlyArray<any>) => Record<string, any> {
474474
const { expansions, args } = mergedTypeResolverInfo;
475475
return (keys: ReadonlyArray<any>): Record<string, any> => {
476-
const newArgs = mergeDeep({}, args);
476+
const newArgs = mergeDeep([{}, args]);
477477
if (expansions) {
478478
for (const expansion of expansions) {
479479
const mappingInstructions = expansion.mappingInstructions;
480480
const expanded: Array<any> = [];
481481
for (const key of keys) {
482-
let newValue = mergeDeep({}, expansion.valuePath);
482+
let newValue = mergeDeep([{}, expansion.valuePath]);
483483
for (const { destinationPath, sourcePath } of mappingInstructions) {
484484
if (destinationPath.length) {
485485
addProperty(newValue, destinationPath, getProperty(key, sourcePath));
@@ -500,7 +500,7 @@ function generateArgsFn(mergedTypeResolverInfo: MergedTypeResolverInfo): (origin
500500
const { mappingInstructions, args, usedProperties } = mergedTypeResolverInfo;
501501

502502
return (originalResult: any): Record<string, any> => {
503-
const newArgs = mergeDeep({}, args);
503+
const newArgs = mergeDeep([{}, args]);
504504
const filteredResult = getProperties(originalResult, usedProperties);
505505
if (mappingInstructions) {
506506
for (const mappingInstruction of mappingInstructions) {
@@ -532,7 +532,7 @@ function buildKeyExpr(key: Array<string>): string {
532532
for (const aliasPart of aliasParts.reverse()) {
533533
object = { [aliasPart]: object };
534534
}
535-
mergedObject = mergeDeep(mergedObject, object);
535+
mergedObject = mergeDeep([mergedObject, object]);
536536
}
537537

538538
return JSON.stringify(mergedObject).replace(/"/g, '');

‎packages/utils/src/mergeDeep.ts

+19-18
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
import { isSome } from './helpers';
2-
import { isScalarType } from 'graphql';
32

43
type BoxedTupleTypes<T extends any[]> = { [P in keyof T]: [T[P]] }[Exclude<keyof T, keyof any[]>];
54
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
65
type UnboxIntersection<T> = T extends { 0: infer U } ? U : never;
76
// eslint-disable-next-line @typescript-eslint/ban-types
8-
export function mergeDeep<T extends object, S extends any[]>(
9-
target: T,
10-
...sources: S
11-
): T & UnboxIntersection<UnionToIntersection<BoxedTupleTypes<S>>> & any {
12-
if (isScalarType(target)) {
13-
return target;
14-
}
7+
export function mergeDeep<S extends any[]>(
8+
sources: S,
9+
respectPrototype = false
10+
): UnboxIntersection<UnionToIntersection<BoxedTupleTypes<S>>> & any {
11+
const target = sources[0] || {};
1512
const output = {};
16-
Object.setPrototypeOf(output, Object.create(Object.getPrototypeOf(target)));
17-
for (const source of [target, ...sources]) {
13+
if (respectPrototype) {
14+
Object.setPrototypeOf(output, Object.create(Object.getPrototypeOf(target)));
15+
}
16+
for (const source of sources) {
1817
if (isObject(target) && isObject(source)) {
19-
const outputPrototype = Object.getPrototypeOf(output);
20-
const sourcePrototype = Object.getPrototypeOf(source);
21-
if (sourcePrototype) {
22-
for (const key of Object.getOwnPropertyNames(sourcePrototype)) {
23-
const descriptor = Object.getOwnPropertyDescriptor(sourcePrototype, key);
24-
if (isSome(descriptor)) {
25-
Object.defineProperty(outputPrototype, key, descriptor);
18+
if (respectPrototype) {
19+
const outputPrototype = Object.getPrototypeOf(output);
20+
const sourcePrototype = Object.getPrototypeOf(source);
21+
if (sourcePrototype) {
22+
for (const key of Object.getOwnPropertyNames(sourcePrototype)) {
23+
const descriptor = Object.getOwnPropertyDescriptor(sourcePrototype, key);
24+
if (isSome(descriptor)) {
25+
Object.defineProperty(outputPrototype, key, descriptor);
26+
}
2627
}
2728
}
2829
}
@@ -32,7 +33,7 @@ export function mergeDeep<T extends object, S extends any[]>(
3233
if (!(key in output)) {
3334
Object.assign(output, { [key]: source[key] });
3435
} else {
35-
output[key] = mergeDeep(output[key], source[key]);
36+
output[key] = mergeDeep([output[key], source[key]] as S, respectPrototype);
3637
}
3738
} else {
3839
Object.assign(output, { [key]: source[key] });
+20-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
import { mergeDeep } from '@graphql-tools/utils'
22

33
describe('mergeDeep', () => {
4+
5+
test('merges deeply', () => {
6+
const x = { a: { one: 1 } }
7+
const y = { a: { two: 2 } }
8+
expect(mergeDeep([x, y])).toEqual({ a: { one: 1, two: 2 } })
9+
})
10+
11+
test('strips property symbols', () => {
12+
const x = {}
13+
const symbol = Symbol('symbol')
14+
x[symbol] = 'value'
15+
const y = { a: 2 }
16+
17+
const merged = mergeDeep([x, y])
18+
expect(merged).toStrictEqual({ a: 2 })
19+
expect(Object.getOwnPropertySymbols(merged)).toEqual([])
20+
})
21+
422
test('merges prototypes', () => {
523
const ClassA = class {
624
a() {
@@ -13,17 +31,11 @@ describe('mergeDeep', () => {
1331
}
1432
}
1533

16-
const merged = mergeDeep(new ClassA(), new ClassB())
34+
const merged = mergeDeep([new ClassA(), new ClassB()], true)
1735
expect(merged.a()).toEqual('a')
1836
expect(merged.b()).toEqual('b')
1937
})
2038

21-
test('merges deeply', () => {
22-
const x = { a: { one: 1 } }
23-
const y = { a: { two: 2 } }
24-
expect(mergeDeep(x, y)).toEqual({ a: { one: 1, two: 2 } })
25-
})
26-
2739
test('merges prototype deeply', () => {
2840
const ClassA = class {
2941
a() {
@@ -36,20 +48,9 @@ describe('mergeDeep', () => {
3648
}
3749
}
3850

39-
const merged = mergeDeep({ one: new ClassA()}, { one: new ClassB()})
51+
const merged = mergeDeep([{ one: new ClassA() }, { one: new ClassB() }], true)
4052
expect(merged.one.a()).toEqual('a')
4153
expect(merged.one.b()).toEqual('b')
4254
expect(merged.a).toBeUndefined()
4355
})
44-
45-
test('strips property symbols', () => {
46-
const x = {}
47-
const symbol = Symbol('symbol')
48-
x[symbol] = 'value'
49-
const y = { a: 2 }
50-
51-
const merged = mergeDeep(x, y)
52-
expect(merged).toStrictEqual({ a: 2 })
53-
expect(Object.getOwnPropertySymbols(merged)).toEqual([])
54-
})
5556
})

0 commit comments

Comments
 (0)
Please sign in to comment.