From 6ec13ab20357a204bb5c164934467908623923dd Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Tue, 6 Feb 2024 15:01:32 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20mark=20types=20whose=20fields=20are=20al?= =?UTF-8?q?l=20inaccessible=20as=20inaccessible=20if=20=E2=80=A6=20(#3927)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/lib/federation-tag-extraction.spec.ts | 55 +++++++++++++++- .../src/lib/federation-tag-extraction.ts | 64 ++++++++----------- ...02-06-schema-contracts-for-federation.mdx} | 2 +- 3 files changed, 80 insertions(+), 41 deletions(-) rename packages/web/docs/src/pages/product-updates/{2024-01-25-schema-contracts-for-federation.mdx => 2024-02-06-schema-contracts-for-federation.mdx} (99%) diff --git a/packages/services/schema/src/lib/federation-tag-extraction.spec.ts b/packages/services/schema/src/lib/federation-tag-extraction.spec.ts index 598a41f2b..a668f9a09 100644 --- a/packages/services/schema/src/lib/federation-tag-extraction.spec.ts +++ b/packages/services/schema/src/lib/federation-tag-extraction.spec.ts @@ -1742,6 +1742,59 @@ describe('applyTagFilterOnSubgraphs', () => { `); }); + test('object type that is only defined in one subgraph is inaccessible', () => { + const filter: Federation2SubgraphDocumentNodeByTagsFilter = { + include: new Set(['tag1']), + exclude: null, + }; + const typeDefs1 = parse(/* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) + + type Query { + helloWorld: String @tag(name: "tag1") + } + `); + + const typeDefs2 = parse(/* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type Query { + users: [User] + } + + type User @key(fields: "id") { + id: ID! + } + `); + + const result = applyTagFilterOnSubgraphs( + [ + { typeDefs: typeDefs1, name: 'subgraph1' }, + { typeDefs: typeDefs2, name: 'subgraph2' }, + ], + filter, + ); + + expect(print(result[0].typeDefs)).toMatchInlineSnapshot(` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) + + type Query { + helloWorld: String + } + `); + expect(print(result[1].typeDefs)).toMatchInlineSnapshot(` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type Query { + users: [User] @federation__inaccessible + } + + type User @key(fields: "id") @federation__inaccessible { + id: ID! @federation__inaccessible + } + `); + }); + test('object types are accessible because at least one field is accessible in one subgraph, but not in another', () => { const filter: Federation2SubgraphDocumentNodeByTagsFilter = { include: new Set(['tag1']), @@ -1778,7 +1831,7 @@ describe('applyTagFilterOnSubgraphs', () => { field2: Type1! @inaccessible } - type Type1 { + type Type1 @inaccessible { field1: String! @inaccessible } `); diff --git a/packages/services/schema/src/lib/federation-tag-extraction.ts b/packages/services/schema/src/lib/federation-tag-extraction.ts index 5bf2a4eda..7cff1c838 100644 --- a/packages/services/schema/src/lib/federation-tag-extraction.ts +++ b/packages/services/schema/src/lib/federation-tag-extraction.ts @@ -224,7 +224,7 @@ export function applyTagFilterToInaccessibleTransformOnSubgraphSchema( filter: Federation2SubgraphDocumentNodeByTagsFilter, ): { typeDefs: DocumentNode; - typesWhereAllFieldsAreInaccessible: Set; + typesWithAllFieldsInaccessible: Map; transformTagDirectives: ReturnType; } { const tagDirectiveName = getFederationTagDirectiveNameForSubgraphSDL(documentNode); @@ -419,47 +419,13 @@ export function applyTagFilterToInaccessibleTransformOnSubgraphSchema( typesWithAllFieldsInaccessibleTracker.delete(rootTypeName); } - const typesWhereAllFieldsAreInaccessible = new Set( - Array.from(typesWithAllFieldsInaccessibleTracker).map(([typeName, isAllFieldsInaccessible]) => - isAllFieldsInaccessible ? typeName : null, - ), - ); - typesWhereAllFieldsAreInaccessible.delete(null); - return { typeDefs, - typesWhereAllFieldsAreInaccessible: typesWhereAllFieldsAreInaccessible as Set, + typesWithAllFieldsInaccessible: typesWithAllFieldsInaccessibleTracker, transformTagDirectives, }; } -function intersectSets(sets: Set[]): Set { - if (sets.length === 0) { - // If the input array is empty, return an empty set - return new Set(); - } - - // Create a copy of the first set to modify - const result = new Set(sets[0]); - - // Iterate through the rest of the sets - for (let i = 1; i < sets.length; i++) { - // Filter the current set and keep only elements present in the result set - result.forEach(value => { - if (!sets[i].has(value)) { - result.delete(value); - } - }); - - // If the result set becomes empty, no need to continue - if (result.size === 0) { - break; - } - } - - return result; -} - function makeTypesFromSetInaccessible( documentNode: DocumentNode, types: Set, @@ -511,9 +477,29 @@ export function applyTagFilterOnSubgraphs< ...applyTagFilterToInaccessibleTransformOnSubgraphSchema(subgraph.typeDefs, filter), })); - const intersectionOfTypesWhereAllFieldsAreInaccessible = intersectSets( - filteredSubgraphs.map(subgraph => subgraph.typesWhereAllFieldsAreInaccessible), - ); + const intersectionOfTypesWhereAllFieldsAreInaccessible = new Set(); + // We need to traverse all subgraphs to find the intersection of types where all fields are inaccessible. + // If a type is not present in any other subgraph, we can safely mark it as inaccessible. + filteredSubgraphs.forEach(subgraph => { + const otherSubgraphs = filteredSubgraphs.filter(sub => sub !== subgraph); + + for (const [type, allFieldsInaccessible] of subgraph.typesWithAllFieldsInaccessible) { + if ( + allFieldsInaccessible && + otherSubgraphs.every( + sub => + !sub.typesWithAllFieldsInaccessible.has(type) || + sub.typesWithAllFieldsInaccessible.get(type) === true, + ) + ) { + intersectionOfTypesWhereAllFieldsAreInaccessible.add(type); + } + // let's not visit this type a second time... + otherSubgraphs.forEach(sub => { + sub.typesWithAllFieldsInaccessible.delete(type); + }); + } + }); if (!intersectionOfTypesWhereAllFieldsAreInaccessible.size) { filteredSubgraphs; diff --git a/packages/web/docs/src/pages/product-updates/2024-01-25-schema-contracts-for-federation.mdx b/packages/web/docs/src/pages/product-updates/2024-02-06-schema-contracts-for-federation.mdx similarity index 99% rename from packages/web/docs/src/pages/product-updates/2024-01-25-schema-contracts-for-federation.mdx rename to packages/web/docs/src/pages/product-updates/2024-02-06-schema-contracts-for-federation.mdx index a07348b7d..e6e0fc9b0 100644 --- a/packages/web/docs/src/pages/product-updates/2024-01-25-schema-contracts-for-federation.mdx +++ b/packages/web/docs/src/pages/product-updates/2024-02-06-schema-contracts-for-federation.mdx @@ -1,7 +1,7 @@ --- title: Schema Contracts for Federation description: We added support schema contracts for Federation projects. -date: 2023-12-05 +date: 2024-02-06 authors: [laurin] ---