fix: mark types whose fields are all inaccessible as inaccessible if … (#3927)

This commit is contained in:
Laurin Quast 2024-02-06 15:01:32 +01:00 committed by GitHub
parent 8ca25fb114
commit 6ec13ab203
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 41 deletions

View file

@ -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
}
`);

View file

@ -224,7 +224,7 @@ export function applyTagFilterToInaccessibleTransformOnSubgraphSchema(
filter: Federation2SubgraphDocumentNodeByTagsFilter,
): {
typeDefs: DocumentNode;
typesWhereAllFieldsAreInaccessible: Set<string>;
typesWithAllFieldsInaccessible: Map<string, boolean>;
transformTagDirectives: ReturnType<typeof createTransformTagDirectives>;
} {
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<string>,
typesWithAllFieldsInaccessible: typesWithAllFieldsInaccessibleTracker,
transformTagDirectives,
};
}
function intersectSets(sets: Set<string>[]): Set<string> {
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<string>,
@ -511,9 +477,29 @@ export function applyTagFilterOnSubgraphs<
...applyTagFilterToInaccessibleTransformOnSubgraphSchema(subgraph.typeDefs, filter),
}));
const intersectionOfTypesWhereAllFieldsAreInaccessible = intersectSets(
filteredSubgraphs.map(subgraph => subgraph.typesWhereAllFieldsAreInaccessible),
);
const intersectionOfTypesWhereAllFieldsAreInaccessible = new Set<string>();
// 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;

View file

@ -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]
---