Skip to content

Commit

Permalink
fix(federation): merge the elements if the shared root field is a list (
Browse files Browse the repository at this point in the history
#6238)

* fix(federation): merge the elements if the shared root field is a list

* Go
  • Loading branch information
ardatan authored Jun 5, 2024
1 parent d9b9c20 commit 0f7059b
Show file tree
Hide file tree
Showing 11 changed files with 94 additions and 24 deletions.
11 changes: 11 additions & 0 deletions .changeset/tame-rabbits-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@graphql-tools/federation': patch
---

Merge the elements of the lists if the root field is shared across different subgraphs

```graphql
type Query {
products: [Product] # If this field is returned by multiple subgraphs, the elements of the lists will be merged
}
```
11 changes: 11 additions & 0 deletions .changeset/thick-grapes-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@graphql-tools/utils': patch
---

If the given objects are arrays with the same length, merge the elements.

```ts
const a = [{ a: 1 }, { b: 2 }];
const b = [{ c: 3 }, { d: 4 }];
const result = mergeDeep(a, b); // [{ a: 1, c: 3 }, { b: 2, d: 4 }]
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"website",
"benchmark/*"
],
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"keywords": [
"GraphQL",
"Apollo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('Error Handling', () => {
if (process.versions['node'].startsWith('18.')) {
expect(errorMessage).toBe('Unexpected end of JSON input');
} else {
expect(errorMessage).toBe(
expect(errorMessage).toContain(
"Expected ',' or '}' after property value in JSON at position 17",
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/federation/src/supergraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export function getStitchingOptionsFromSupergraphSdl(
return jobs[0];
}
if (hasPromise) {
return Promise.all(jobs).then(results => mergeDeep(results));
return Promise.all(jobs).then(results => mergeDeep(results, false, true, true));
}
return mergeDeep(jobs);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
]
}
},
"plan": "\n QueryPlan {\n Parallel {\n Sequence {\n Fetch(service: \"a\") {\n {\n users {\n __typename\n id\n }\n }\n },\n Flatten(path: \"users.@\") {\n Fetch(service: \"b\") {\n {\n ... on User {\n __typename\n id\n }\n } =>\n {\n ... on User {\n name\n }\n }\n },\n },\n },\n Fetch(service: \"b\") {\n {\n accounts {\n __typename\n ... on User {\n id\n name\n }\n ... on Admin {\n id__alias_0: id\n photo\n }\n }\n }\n },\n },\n }\n "
"plan": "\n QueryPlan {\n Parallel {\n Sequence {\n Fetch(service: \"a\") {\n {\n users {\n __typename\n id\n }\n }\n },\n Flatten(path: \"users.@\") {\n Fetch(service: \"b\") {\n {\n ... on User {\n __typename\n id\n }\n } =>\n {\n ... on User {\n name\n }\n }\n },\n },\n },\n Fetch(service: \"b\") {\n {\n accounts {\n __typename\n ... on User {\n id\n name\n }\n ... on Admin {\n # NOTE:\n # User.id and Admin.id have similar output types,\n # but one returns a non-nullable field and the other a nullable field.\n # To avoid a GraphQL error, we need to alias the field.\n id__alias_0: id\n photo\n }\n }\n }\n },\n },\n }\n "
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"plan": "\n QueryPlan {\n Sequence {\n Fetch(service: \"a\") {\n {\n product(id: \"p1\") {\n __typename\n id\n name\n price\n }\n }\n },\n Flatten(path: \"product\") {\n Fetch(service: \"b\") {\n {\n ... on Product {\n __typename\n id\n price\n }\n } =>\n {\n ... on Product {\n isExpensive\n isAvailable\n }\n }\n },\n },\n },\n }\n "
},
{
"query": "\n mutation {\n five: add(num: 5, requestId: \"7c8c0a7323709\")\n ten: multiply(by: 2, requestId: \"7c8c0a7323709\")\n twelve: add(num: 2, requestId: \"7c8c0a7323709\")\n final: delete(requestId: \"7c8c0a7323709\")\n }\n ",
"query": "\n mutation {\n five: add(num: 5, requestId: \"818c6d2a6f5ec\")\n ten: multiply(by: 2, requestId: \"818c6d2a6f5ec\")\n twelve: add(num: 2, requestId: \"818c6d2a6f5ec\")\n final: delete(requestId: \"818c6d2a6f5ec\")\n }\n ",
"expected": {
"data": {
"five": 5,
Expand All @@ -38,6 +38,6 @@
"final": 12
}
},
"plan": "\n QueryPlan {\n Sequence {\n Fetch(service: \"c\") {\n {\n five: add(num: 5, requestId: \"7c8c0a7323709\")\n }\n },\n Fetch(service: \"a\") {\n {\n ten: multiply(by: 2, requestId: \"7c8c0a7323709\")\n }\n },\n Fetch(service: \"c\") {\n {\n twelve: add(num: 2, requestId: \"7c8c0a7323709\")\n }\n },\n Fetch(service: \"b\") {\n {\n final: delete(requestId: \"7c8c0a7323709\")\n }\n },\n },\n }\n "
"plan": "\n QueryPlan {\n Sequence {\n Fetch(service: \"c\") {\n {\n five: add(num: 5, requestId: \"818c6d2a6f5ec\")\n }\n },\n Fetch(service: \"a\") {\n {\n ten: multiply(by: 2, requestId: \"818c6d2a6f5ec\")\n }\n },\n Fetch(service: \"c\") {\n {\n twelve: add(num: 2, requestId: \"818c6d2a6f5ec\")\n }\n },\n Fetch(service: \"b\") {\n {\n final: delete(requestId: \"818c6d2a6f5ec\")\n }\n },\n },\n }\n "
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ type Query
@join__type(graph: PRICE)
{
product: Product
products: [Product]
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,21 @@
}
},
"plan": "\n QueryPlan {\n # NOTE\n # What's interesting here is that the most efficient way to fetch the data\n # is to make parallel calls to Query.products in each service.\n # Each service has a different field that it can provide.\n Parallel {\n Fetch(service: \"name\") {\n {\n product {\n name\n }\n }\n },\n Fetch(service: \"category\") {\n {\n product {\n category\n id\n }\n }\n },\n Fetch(service: \"price\") {\n {\n product {\n price\n }\n }\n },\n },\n }\n "
},
{
"query": "\n query {\n products {\n id\n name\n category\n price\n }\n }\n ",
"expected": {
"data": {
"products": [
{
"id": "1",
"name": "Product 1",
"price": 100,
"category": "Category 1"
}
]
}
},
"plan": "\n QueryPlan {\n # NOTE\n # What's interesting here is that the most efficient way to fetch the data\n # is to make parallel calls to Query.products in each service.\n # Each service has a different field that it can provide.\n Parallel {\n Fetch(service: \"name\") {\n {\n products {\n name\n }\n }\n },\n Fetch(service: \"category\") {\n {\n products {\n category\n id\n }\n }\n },\n Fetch(service: \"price\") {\n {\n products {\n price\n }\n }\n },\n },\n }\n "
}
]
56 changes: 39 additions & 17 deletions packages/utils/src/mergeDeep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,38 @@ export function mergeDeep<S extends any[]>(
respectArrays = false,
respectArrayLength = false,
): UnboxIntersection<UnionToIntersection<BoxedTupleTypes<S>>> & any {
const target = sources[0] || {};
if (respectArrays && respectArrayLength) {
let expectedLength: number | undefined;
const areArraysInTheSameLength = sources.every(source => {
if (Array.isArray(source)) {
if (expectedLength === undefined) {
expectedLength = source.length;
return true;
} else if (expectedLength === source.length) {
return true;
}
}
return false;
});

if (areArraysInTheSameLength) {
return new Array(expectedLength).fill(null).map((_, index) =>
mergeDeep(
sources.map(source => source[index]),
respectPrototype,
respectArrays,
respectArrayLength,
),
);
}
}

const output = {};
if (respectPrototype) {
Object.setPrototypeOf(output, Object.create(Object.getPrototypeOf(target)));
Object.setPrototypeOf(output, Object.create(Object.getPrototypeOf(sources[0])));
}
for (const source of sources) {
if (isObject(target) && isObject(source)) {
if (isObject(source)) {
if (respectPrototype) {
const outputPrototype = Object.getPrototypeOf(output);
const sourcePrototype = Object.getPrototypeOf(source);
Expand All @@ -41,31 +66,28 @@ export function mergeDeep<S extends any[]>(
[output[key], source[key]] as S,
respectPrototype,
respectArrays,
respectArrayLength,
);
}
} else if (respectArrays && Array.isArray(output[key])) {
if (Array.isArray(source[key])) {
output[key].push(...source[key]);
if (respectArrayLength && output[key].length === source[key].length) {
output[key] = mergeDeep(
[output[key], source[key]] as S,
respectPrototype,
respectArrays,
respectArrayLength,
);
} else {
output[key].push(...source[key]);
}
} else {
output[key].push(source[key]);
}
} else {
Object.assign(output, { [key]: source[key] });
}
}
} else if (respectArrays && Array.isArray(target)) {
if (Array.isArray(source)) {
if (respectArrayLength && source.length === target.length) {
return target.map((targetElem, i) =>
mergeDeep([targetElem, source[i]], respectPrototype, respectArrays, respectArrayLength),
);
}
target.push(...source);
} else {
target.push(source);
}
} else if (respectArrays && Array.isArray(source)) {
return [target, ...source];
}
}
return output;
Expand Down
12 changes: 10 additions & 2 deletions packages/utils/tests/mergeDeep.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,16 @@ describe('mergeDeep', () => {
});

it('merges arrays', () => {
const x = { a: [1, 2] };
const x = { a: [1, 2, 5] };
const y = { a: [3, 4] };
expect(mergeDeep([x, y], false, true)).toEqual({ a: [1, 2, 3, 4] });
expect(mergeDeep([x, y], false, true)).toEqual({ a: [1, 2, 5, 3, 4] });
});
it('merges arrays with the same length', () => {
const x = [{ a: 1 }, { b: 2 }];
const y = [{ c: 3 }, { d: 4 }];
expect(mergeDeep([x, y], false, true, true)).toEqual([
{ a: 1, c: 3 },
{ b: 2, d: 4 },
]);
});
});

0 comments on commit 0f7059b

Please sign in to comment.