diff --git a/.changeset/dirty-humans-prove.md b/.changeset/dirty-humans-prove.md new file mode 100644 index 00000000..8e3fdbd5 --- /dev/null +++ b/.changeset/dirty-humans-prove.md @@ -0,0 +1,5 @@ +--- +"@theguild/federation-composition": patch +--- + +tag extraction respects @external on parent diff --git a/__tests__/composition.spec.ts b/__tests__/composition.spec.ts index cafab7c3..f2d056a8 100644 --- a/__tests__/composition.spec.ts +++ b/__tests__/composition.spec.ts @@ -7382,8 +7382,8 @@ testImplementations((api) => { typeDefs: parse(/* GraphQL */ ` extend schema @link( - url: "https://specs.apollo.dev/federation/v2.3" - import: ["@key", "@shareable"] + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@shareable"] ) type Note @key(fields: "id") @shareable { id: ID! @@ -7391,16 +7391,16 @@ testImplementations((api) => { author: User } type User @key(fields: "id") { - id: ID! - name: String + id: ID! + name: String } - type PrivateNote @key(fields: "id") @shareable { - id: ID! - note: Note - } - type Query { - note: Note @shareable - privateNote: PrivateNote @shareable + type PrivateNote @key(fields: "id") @shareable { + id: ID! + note: Note + } + type Query { + note: Note @shareable + privateNote: PrivateNote @shareable } `), }, @@ -7425,9 +7425,9 @@ testImplementations((api) => { result = api.composeServices([ { name: "foo", - typeDefs: parse(/* GraphQL */ ` + typeDefs: parse(/* GraphQL */ ` extend schema - @link( + @link( url: "https://specs.apollo.dev/federation/v2.3" import: ["@key", "@external", "@provides", "@shareable"] ) @@ -7442,8 +7442,8 @@ testImplementations((api) => { type PrivateNote @key(fields: "id") @shareable { id: ID! note: Note @provides(fields: "name author { id }") - } - type Query { + } + type Query { note: Note @shareable privateNote: PrivateNote @shareable } @@ -7461,8 +7461,8 @@ testImplementations((api) => { id: ID! name: String author: User - } - type User @key(fields: "id") { + } + type User @key(fields: "id") { id: ID! name: String } @@ -7490,7 +7490,7 @@ testImplementations((api) => { type Note @key(fields: "id") @shareable { id: ID! tag: String - } + } `), }, ]); @@ -7510,7 +7510,7 @@ testImplementations((api) => { @join__field(external: true, graph: FOO) @join__field(graph: BAR) tag: String @join__field(graph: BAZ) - } + } `); }); diff --git a/__tests__/contracts/federation-tag-extraction.spec.ts b/__tests__/contracts/federation-tag-extraction.spec.ts index 5ad21020..69601740 100644 --- a/__tests__/contracts/federation-tag-extraction.spec.ts +++ b/__tests__/contracts/federation-tag-extraction.spec.ts @@ -395,6 +395,88 @@ describe("applyTagFilterToInaccessibleTransformOnSubgraphSchema", () => { `); }); + test("include of object type field if @external is applied to field", () => { + const filter: SchemaContractFilter = { + include: new Set(["tag1"]), + exclude: new Set(), + }; + const sdl = parse(/* GraphQL */ ` + schema + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@tag", "@external"] + ) { + query: Query + } + + type Query { + field1: String! @tag(name: "tag1") @requires(fields: "field2") + field2: String! @external + } + `); + + const output = applyTagFilterToInaccessibleTransformOnSubgraphSchema( + sdl, + buildSchemaCoordinateTagRegister([sdl]), + filter, + ); + + expect(print(output.typeDefs)).toMatchInlineSnapshot(` + "schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag", "@external"]) { + query: Query + } + + type Query { + field1: String! @requires(fields: "field2") + field2: String! @external + }" + `); + }); + + test("include of object type field if @external is applied to object", () => { + const filter: SchemaContractFilter = { + include: new Set(["tag1"]), + exclude: new Set(), + }; + const sdl = parse(/* GraphQL */ ` + schema + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@tag", "@external"] + ) { + query: Query + } + + extend type Query @external { + field2: String! + } + + type Query { + field1: String! @tag(name: "tag1") @requires(fields: "field2") + } + `); + + const output = applyTagFilterToInaccessibleTransformOnSubgraphSchema( + sdl, + buildSchemaCoordinateTagRegister([sdl]), + filter, + ); + + expect(print(output.typeDefs)).toMatchInlineSnapshot(` + "schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag", "@external"]) { + query: Query + } + + extend type Query @external { + field2: String! + } + + type Query { + field1: String! @requires(fields: "field2") + }" + `); + }); + test("object type is excluded even if one of its fields is included", () => { const filter: SchemaContractFilter = { include: new Set(["tag1"]), diff --git a/package.json b/package.json index afa711f0..afe36a31 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "typecheck": "tsc --noEmit --project tsconfig.build.json" }, "peerDependencies": { - "graphql": "^16.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "dependencies": { "constant-case": "^3.0.4", diff --git a/src/contracts/tag-extraction.ts b/src/contracts/tag-extraction.ts index ebb4acc6..14a2df72 100644 --- a/src/contracts/tag-extraction.ts +++ b/src/contracts/tag-extraction.ts @@ -458,7 +458,8 @@ export function applyTagFilterToInaccessibleTransformOnSubgraphSchema( if ( fieldNode.directives?.find( (d) => d.name.value === externalDirectiveName, - ) + ) || + node.directives?.find((d) => d.name.value === externalDirectiveName) ) { return fieldNode; } diff --git a/src/subgraph/state.ts b/src/subgraph/state.ts index a75ed20d..2b8ab615 100644 --- a/src/subgraph/state.ts +++ b/src/subgraph/state.ts @@ -8,7 +8,7 @@ import { Kind, ObjectTypeDefinitionNode, ObjectTypeExtensionNode, - OperationTypeNode, + type OperationTypeNode, SchemaDefinitionNode, specifiedDirectives as specifiedDirectiveTypes, specifiedScalarTypes, @@ -398,17 +398,17 @@ export function createSubgraphStateBuilder( const expectedQueryTypeName = decideOnRootTypeName( schemaDef, - OperationTypeNode.QUERY, + 'query' as OperationTypeNode, "Query", ); const expectedMutationTypeName = decideOnRootTypeName( schemaDef, - OperationTypeNode.MUTATION, + 'mutation' as OperationTypeNode, "Mutation", ); const expectedSubscriptionTypeName = decideOnRootTypeName( schemaDef, - OperationTypeNode.SUBSCRIPTION, + 'subscription' as OperationTypeNode, "Subscription", ); diff --git a/src/subgraph/validation/rules/known-directives-rule.ts b/src/subgraph/validation/rules/known-directives-rule.ts index 33fafe2b..2624ee14 100644 --- a/src/subgraph/validation/rules/known-directives-rule.ts +++ b/src/subgraph/validation/rules/known-directives-rule.ts @@ -5,7 +5,7 @@ import { DocumentNode, GraphQLError, Kind, - OperationTypeNode, + type OperationTypeNode, specifiedDirectives, } from "graphql"; @@ -135,11 +135,11 @@ function getDirectiveLocationForOperation( operation: OperationTypeNode, ): DirectiveLocation { switch (operation) { - case OperationTypeNode.QUERY: + case 'query' as OperationTypeNode.QUERY: return DirectiveLocation.QUERY; - case OperationTypeNode.MUTATION: + case 'mutation' as OperationTypeNode.MUTATION: return DirectiveLocation.MUTATION; - case OperationTypeNode.SUBSCRIPTION: + case 'subscription' as OperationTypeNode.SUBSCRIPTION: return DirectiveLocation.SUBSCRIPTION; } } diff --git a/src/subgraph/validation/rules/query-root-type-inaccessible-rule.ts b/src/subgraph/validation/rules/query-root-type-inaccessible-rule.ts index 86154e3a..56f0a42f 100644 --- a/src/subgraph/validation/rules/query-root-type-inaccessible-rule.ts +++ b/src/subgraph/validation/rules/query-root-type-inaccessible-rule.ts @@ -1,4 +1,4 @@ -import { ASTVisitor, GraphQLError, OperationTypeNode } from "graphql"; +import { ASTVisitor, GraphQLError, type OperationTypeNode } from "graphql"; import type { SubgraphValidationContext } from "../validation-context.js"; export function QueryRootTypeInaccessibleRule( @@ -10,7 +10,7 @@ export function QueryRootTypeInaccessibleRule( SchemaDefinition(node) { const nonQueryType = node.operationTypes?.find( (operationType) => - operationType.operation === OperationTypeNode.QUERY && + operationType.operation === 'query' && operationType.type.name.value !== "Query", ); @@ -21,7 +21,7 @@ export function QueryRootTypeInaccessibleRule( SchemaExtension(node) { const nonQueryType = node.operationTypes?.find( (operationType) => - operationType.operation === OperationTypeNode.QUERY && + operationType.operation === 'query' && operationType.type.name.value !== "Query", ); diff --git a/src/subgraph/validation/rules/root-type-used-rule.ts b/src/subgraph/validation/rules/root-type-used-rule.ts index 0e71390d..825a3017 100644 --- a/src/subgraph/validation/rules/root-type-used-rule.ts +++ b/src/subgraph/validation/rules/root-type-used-rule.ts @@ -3,7 +3,7 @@ import { DefinitionNode, GraphQLError, isTypeDefinitionNode, - OperationTypeNode, + type OperationTypeNode, SchemaDefinitionNode, SchemaExtensionNode, } from "graphql"; @@ -26,13 +26,13 @@ function findDefaultRootTypes(definitions: readonly DefinitionNode[]) { if (isTypeDefinitionNode(definition)) { if (definition.name.value === "Query") { foundRootTypes.query = "Query"; - found.add(OperationTypeNode.QUERY); + found.add('query' as OperationTypeNode); } else if (definition.name.value === "Mutation") { foundRootTypes.mutation = "Mutation"; - found.add(OperationTypeNode.MUTATION); + found.add('mutation' as OperationTypeNode); } else if (definition.name.value === "Subscription") { foundRootTypes.subscription = "Subscription"; - found.add(OperationTypeNode.SUBSCRIPTION); + found.add('subscription' as OperationTypeNode); } } } diff --git a/src/subgraph/validation/validate-subgraph.ts b/src/subgraph/validation/validate-subgraph.ts index 4abcc302..9ab6503f 100644 --- a/src/subgraph/validation/validate-subgraph.ts +++ b/src/subgraph/validation/validate-subgraph.ts @@ -8,7 +8,6 @@ import { Kind, ObjectTypeDefinitionNode, ObjectTypeExtensionNode, - OperationTypeNode, parse, SchemaDefinitionNode, SchemaExtensionNode, @@ -541,13 +540,13 @@ function cleanSubgraphTypeDefsFromSubgraphSpec(typeDefs: DocumentNode) { (node.kind === Kind.SCHEMA_DEFINITION || node.kind === Kind.SCHEMA_EXTENSION) && node.operationTypes?.some( - (op) => op.operation === OperationTypeNode.QUERY, + (op) => op.operation === 'query', ), ) as SchemaDefinitionNode | SchemaExtensionNode | undefined; const queryTypeName = schemaDef?.operationTypes?.find( - (op) => op.operation === OperationTypeNode.QUERY, + (op) => op.operation === 'query', )?.type.name.value ?? "Query"; (typeDefs.definitions as unknown as DefinitionNode[]) = diff --git a/src/supergraph/composition/ast.ts b/src/supergraph/composition/ast.ts index a8deef8a..d2e90f71 100644 --- a/src/supergraph/composition/ast.ts +++ b/src/supergraph/composition/ast.ts @@ -21,7 +21,7 @@ import { NamedTypeNode, ObjectTypeDefinitionNode, OperationTypeDefinitionNode, - OperationTypeNode, + type OperationTypeNode, parseConstValue, parseType, ScalarTypeDefinitionNode, @@ -77,21 +77,21 @@ export function createSchemaNode(schema: { schema.query ? { kind: Kind.OPERATION_TYPE_DEFINITION, - operation: OperationTypeNode.QUERY, + operation: 'query' as OperationTypeNode, type: createNamedTypeNode(schema.query), } : [], schema.mutation ? { kind: Kind.OPERATION_TYPE_DEFINITION, - operation: OperationTypeNode.MUTATION, + operation: 'mutation' as OperationTypeNode, type: createNamedTypeNode(schema.mutation), } : [], schema.subscription ? { kind: Kind.OPERATION_TYPE_DEFINITION, - operation: OperationTypeNode.SUBSCRIPTION, + operation: 'subscription' as OperationTypeNode, type: createNamedTypeNode(schema.subscription), } : [], diff --git a/src/supergraph/validation/rules/satisfiablity-rule.ts b/src/supergraph/validation/rules/satisfiablity-rule.ts index e3dd016c..894815bd 100644 --- a/src/supergraph/validation/rules/satisfiablity-rule.ts +++ b/src/supergraph/validation/rules/satisfiablity-rule.ts @@ -2,7 +2,6 @@ import { GraphQLError, Kind, ListValueNode, - OperationTypeNode, print, specifiedScalarTypes, ValueNode, diff --git a/src/supergraph/validation/rules/satisfiablity/supergraph.ts b/src/supergraph/validation/rules/satisfiablity/supergraph.ts index 62534060..3d8a1fee 100644 --- a/src/supergraph/validation/rules/satisfiablity/supergraph.ts +++ b/src/supergraph/validation/rules/satisfiablity/supergraph.ts @@ -1,4 +1,4 @@ -import { OperationTypeNode } from "graphql"; +import type { OperationTypeNode } from "graphql"; import { Logger, LoggerContext } from "../../../../utils/logger.js"; import type { SupergraphState } from "../../../state.js"; import { MERGEDGRAPH_ID, SUPERGRAPH_ID } from "./constants.js"; diff --git a/src/supergraph/validation/rules/satisfiablity/walker.ts b/src/supergraph/validation/rules/satisfiablity/walker.ts index 686bcfc7..9afb05f6 100644 --- a/src/supergraph/validation/rules/satisfiablity/walker.ts +++ b/src/supergraph/validation/rules/satisfiablity/walker.ts @@ -1,4 +1,4 @@ -import { OperationTypeNode } from "graphql"; +import type { OperationTypeNode } from "graphql"; import type { Logger } from "../../../../utils/logger.js"; import { isAbstractEdge, isFieldEdge, type Edge } from "./edge.js"; import { LazyErrors, SatisfiabilityError } from "./errors.js"; @@ -119,9 +119,9 @@ export class Walker { } const rootNode = this.supergraph.nodeOf( - operationType === OperationTypeNode.QUERY + operationType === 'query' ? "Query" - : operationType === OperationTypeNode.MUTATION + : operationType === 'mutation' ? "Mutation" : "Subscription", false,