diff --git a/.changeset/curvy-flowers-play.md b/.changeset/curvy-flowers-play.md new file mode 100644 index 00000000000..d92b4b97f28 --- /dev/null +++ b/.changeset/curvy-flowers-play.md @@ -0,0 +1,21 @@ +--- +'@graphql-tools/federation': patch +--- + +If there are repeated computed fields like below, project the data for the computed fields for each `fields` and merge them correctly. +And if they are array as in `userOrders`, merge them by respecting the order (the second one can have `price` maybe). + +```graphql +type UserOrder @key(fields: "id") { + id: ID! + status: String! + price: Int! +} + +type User @key(fields: "id") { + id: ID! + userOrders: [UserOrder!] @external + totalOrdersPrices: Int @requires(fields: "userOrders { id }") + aggregatedOrdersByStatus: Int @requires(fields: "userOrders { id }") +} +``` diff --git a/packages/federation/src/supergraph.ts b/packages/federation/src/supergraph.ts index a30aa2ae3e2..dfc48cf1ab7 100644 --- a/packages/federation/src/supergraph.ts +++ b/packages/federation/src/supergraph.ts @@ -658,11 +658,11 @@ export function getStitchingOptionsFromSupergraphSdl( for (const [typeName, keys] of typeNameKeyMap) { const mergedTypeConfig: MergedTypeConfig = (mergeConfig[typeName] = {}); const fieldsKeyMap = typeNameFieldsKeyMap?.get(typeName); - const extraKeys: string[] = []; + const extraKeys = new Set(); if (fieldsKeyMap) { const fieldsConfig: Record = (mergedTypeConfig.fields = {}); for (const [fieldName, fieldNameKey] of fieldsKeyMap) { - extraKeys.push(fieldNameKey); + extraKeys.add(fieldNameKey); fieldsConfig[fieldName] = { selectionSet: `{ ${fieldNameKey} }`, computed: true, diff --git a/packages/federation/src/utils.ts b/packages/federation/src/utils.ts index 176769720df..9fa0b30a429 100644 --- a/packages/federation/src/utils.ts +++ b/packages/federation/src/utils.ts @@ -22,12 +22,17 @@ export function projectDataSelectionSet(data: any, selectionSet?: SelectionSetNo for (const selection of selectionSet.selections) { if (selection.kind === Kind.FIELD) { const key = selection.name.value; - if (data.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(data, key)) { const projectedKeyData = projectDataSelectionSet(data[key], selection.selectionSet); if (projectedData[key]) { - projectedData[key] = mergeDeep([projectedData[key], projectedKeyData]); + projectedData[key] = mergeDeep( + [projectedData[key], projectedKeyData], + undefined, + true, + true, + ); } else { - projectedData[key] = projectDataSelectionSet(data[key], selection.selectionSet); + projectedData[key] = projectedKeyData; } } } else if (selection.kind === Kind.INLINE_FRAGMENT) { @@ -40,7 +45,12 @@ export function projectDataSelectionSet(data: any, selectionSet?: SelectionSetNo } Object.assign( projectedData, - mergeDeep([projectedData, projectDataSelectionSet(data, selection.selectionSet)]), + mergeDeep( + [projectedData, projectDataSelectionSet(data, selection.selectionSet)], + undefined, + true, + true, + ), ); } } diff --git a/packages/federation/test/getKeyFnForFederation.test.ts b/packages/federation/test/getKeyFnForFederation.test.ts index 89ee7835f50..041ec1a3bfd 100644 --- a/packages/federation/test/getKeyFnForFederation.test.ts +++ b/packages/federation/test/getKeyFnForFederation.test.ts @@ -25,4 +25,42 @@ describe('getKeyFnForFederation', () => { price: 100, }); }); + it('with repeating fields returning arrays', () => { + const keys = ['userOrders { id }', 'userOrders { tag }']; + const data = { + __typename: 'User', + id: 1, + name: 'Test', + userOrders: [ + { + __typename: 'UserOrder', + id: '1', + total: 100, + tag: 'Test1', + }, + { + __typename: 'UserOrder', + id: '2', + total: 400, + tag: 'Test2', + }, + ], + }; + const keyFn = getKeyFnForFederation('User', keys); + expect(keyFn(data)).toEqual({ + __typename: 'User', + userOrders: [ + { + __typename: 'UserOrder', + id: '1', + tag: 'Test1', + }, + { + __typename: 'UserOrder', + id: '2', + tag: 'Test2', + }, + ], + }); + }); });