From c7d0e8e6b501cd21ce3a842b0f8728d23af799c6 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Tue, 3 Sep 2024 11:05:42 -0700 Subject: [PATCH 01/10] Ignore non-resolvable keys in locally satisfiable key checking (#3137) This PR fixes a bug where `getLocallySatisfiableKey()` would mistakenly consider non-resolvable keys, which would sometimes result in self jumps for `@requires` using non-resolvable keys. --- .changeset/rich-tips-study.md | 6 ++ query-graphs-js/src/graphPath.ts | 3 + .../src/__tests__/buildPlan.test.ts | 91 +++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 .changeset/rich-tips-study.md diff --git a/.changeset/rich-tips-study.md b/.changeset/rich-tips-study.md new file mode 100644 index 000000000..1bc53bdc6 --- /dev/null +++ b/.changeset/rich-tips-study.md @@ -0,0 +1,6 @@ +--- +"@apollo/query-planner": patch +"@apollo/query-graphs": patch +--- + +Ignore non-resolvable keys when adding a subgraph jump for `@requires`/`@fromContext`. diff --git a/query-graphs-js/src/graphPath.ts b/query-graphs-js/src/graphPath.ts index e37751028..610065d2a 100644 --- a/query-graphs-js/src/graphPath.ts +++ b/query-graphs-js/src/graphPath.ts @@ -1930,6 +1930,9 @@ export function getLocallySatisfiableKey(graph: QueryGraph, typeVertex: Vertex): assert(metadata, () => `Could not find federation metadata for source ${typeVertex.source}`); const keyDirective = metadata.keyDirective(); for (const key of type.appliedDirectivesOf(keyDirective)) { + if (!(key.arguments().resolvable ?? true)) { + continue; + } const selection = parseFieldSetArgument({ parentType: type, directive: key }); if (!metadata.selectionSelectsAnyExternalField(selection)) { return selection; diff --git a/query-planner-js/src/__tests__/buildPlan.test.ts b/query-planner-js/src/__tests__/buildPlan.test.ts index af995c066..2f59fbdd6 100644 --- a/query-planner-js/src/__tests__/buildPlan.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.test.ts @@ -2986,6 +2986,97 @@ describe('@requires', () => { } `); }); + + it('ignores non-resolvable keys when inserting self jump for @requires', () => { + const subgraph1 = { + name: 'A', + typeDefs: gql` + type Query { + t: T + } + + type T @key(fields: "id1", resolvable: false) @key(fields: "id2 id3") { + id1: ID! + id2: ID! + id3: ID! + req: Int @external + v: Int @requires(fields: "req") + } + `, + }; + + const subgraph2 = { + name: 'B', + typeDefs: gql` + type T @key(fields: "id1") { + id1: ID! + req: Int + } + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); + const operation = operationFromDocument( + api, + gql` + { + t { + v + } + } + `, + ); + + const plan = queryPlanner.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "A") { + { + t { + __typename + id1 + id2 + id3 + } + } + }, + Flatten(path: "t") { + Fetch(service: "B") { + { + ... on T { + __typename + id1 + } + } => + { + ... on T { + req + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "A") { + { + ... on T { + __typename + req + id2 + id3 + } + } => + { + ... on T { + v + } + } + }, + }, + }, + } + `); + }); }); describe('fetch operation names', () => { From b8e4ab5352a4dfd262af49493fdd42e86e5e3d99 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Tue, 3 Sep 2024 11:59:37 -0700 Subject: [PATCH 02/10] Handle edge cases with subgraph extraction logic (#3136) While reviewing recent code changes that released in `2.9.0`, I noticed a few bugs in the code. This PR contains fixes for subgraph extraction bugs. Specifically, this PR updates subgraph extraction to use pre-existing functions/patterns instead of repeating logic, to avoid copy/pasting and bugs/divergence (the copied logic wasn't looking at spec renaming, for example). --- .changeset/light-ties-chew.md | 5 + .../src/extractSubgraphsFromSupergraph.ts | 165 ++++++++---------- internals-js/src/specs/costSpec.ts | 10 +- internals-js/src/supergraphs.ts | 34 +++- 4 files changed, 118 insertions(+), 96 deletions(-) create mode 100644 .changeset/light-ties-chew.md diff --git a/.changeset/light-ties-chew.md b/.changeset/light-ties-chew.md new file mode 100644 index 000000000..805c9a403 --- /dev/null +++ b/.changeset/light-ties-chew.md @@ -0,0 +1,5 @@ +--- +"@apollo/federation-internals": patch +--- + +Fix edge cases for subgraph extraction logic when using spec renaming or specs URLs that look similar to `specs.apollo.dev`. diff --git a/internals-js/src/extractSubgraphsFromSupergraph.ts b/internals-js/src/extractSubgraphsFromSupergraph.ts index 3d412d039..421c66bee 100644 --- a/internals-js/src/extractSubgraphsFromSupergraph.ts +++ b/internals-js/src/extractSubgraphsFromSupergraph.ts @@ -40,7 +40,7 @@ import { parseSelectionSet } from "./operations"; import fs from 'fs'; import path from 'path'; import { validateStringContainsBoolean } from "./utils"; -import { CONTEXT_VERSIONS, ContextSpecDefinition, DirectiveDefinition, FeatureUrl, FederationDirectiveName, SchemaElement, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from "."; +import { ContextSpecDefinition, CostSpecDefinition, SchemaElement, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from "."; function filteredTypes( supergraph: Schema, @@ -194,7 +194,7 @@ function typesUsedInFederationDirective(fieldSet: string | undefined, parentType } export function extractSubgraphsFromSupergraph(supergraph: Schema, validateExtractedSubgraphs: boolean = true): [Subgraphs, Map] { - const [coreFeatures, joinSpec] = validateSupergraph(supergraph); + const [coreFeatures, joinSpec, contextSpec, costSpec] = validateSupergraph(supergraph); const isFed1 = joinSpec.version.equals(new FeatureVersion(0, 1)); try { // We first collect the subgraphs (creating an empty schema that we'll populate next for each). @@ -224,13 +224,13 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema, validateExtra } const types = filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition); - const originalDirectiveNames = getApolloDirectiveNames(supergraph); const args: ExtractArguments = { supergraph, subgraphs, joinSpec, + contextSpec, + costSpec, filteredTypes: types, - originalDirectiveNames, getSubgraph, getSubgraphEnumValue, }; @@ -293,8 +293,9 @@ type ExtractArguments = { supergraph: Schema, subgraphs: Subgraphs, joinSpec: JoinSpecDefinition, + contextSpec: ContextSpecDefinition | undefined, + costSpec: CostSpecDefinition | undefined, filteredTypes: NamedType[], - originalDirectiveNames: Record, getSubgraph: (application: Directive) => Subgraph | undefined, getSubgraphEnumValue: (subgraphName: string) => string } @@ -352,7 +353,9 @@ function addAllEmptySubgraphTypes(args: ExtractArguments): TypesInfo { const subgraph = getSubgraph(application); assert(subgraph, () => `Should have found the subgraph for ${application}`); const subgraphType = subgraph.schema.addType(newNamedType(type.kind, type.name)); - propagateDemandControlDirectives(type, subgraphType, subgraph, args.originalDirectiveNames); + if (args.costSpec) { + propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec); + } } break; } @@ -401,17 +404,8 @@ function addEmptyType( } } } - - const coreFeatures = supergraph.coreFeatures; - assert(coreFeatures, 'Should have core features'); - const contextFeature = coreFeatures.getByIdentity(ContextSpecDefinition.identity); - let supergraphContextDirective: DirectiveDefinition<{ name: string}> | undefined; - if (contextFeature) { - const contextSpec = CONTEXT_VERSIONS.find(contextFeature.url.version); - assert(contextSpec, 'Should have context spec'); - supergraphContextDirective = contextSpec.contextDirective(supergraph); - } - + + const supergraphContextDirective = args.contextSpec?.contextDirective(supergraph); if (supergraphContextDirective) { const contextApplications = type.appliedDirectivesOf(supergraphContextDirective); // for every application, apply the context directive to the correct subgraph @@ -438,8 +432,6 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo 1; for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) { - addSubgraphField({ field, type: subgraphType, subgraph, isShareable, originalDirectiveNames }); + addSubgraphField({ + field, + type: subgraphType, + subgraph, + isShareable, + costSpec: args.costSpec + }); } } else { const isShareable = isObjectType(type) @@ -478,58 +478,21 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo renamedCost` with the `@` prefix removed. - * - * If the directive is imported under its default name, that also results in an entry. So, - * ```graphql - * @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) - * ``` - * results in a map entry of `cost -> cost`. This duals as a way to check if a directive - * is included in the supergraph schema. - * - * **Important:** This map does _not_ include directives imported from identities other - * than `specs.apollo.dev`. This helps us avoid extracting directives to subgraphs - * when a custom directive's name conflicts with that of a default one. - */ -function getApolloDirectiveNames(supergraph: Schema): Record { - const originalDirectiveNames: Record = {}; - for (const linkDirective of supergraph.schemaDefinition.appliedDirectivesOf("link")) { - if (linkDirective.arguments().url && linkDirective.arguments().import) { - const url = FeatureUrl.maybeParse(linkDirective.arguments().url); - if (!url?.identity.includes("specs.apollo.dev")) { - continue; - } - - for (const importedDirective of linkDirective.arguments().import) { - if (importedDirective.name && importedDirective.as) { - originalDirectiveNames[importedDirective.name.replace('@', '')] = importedDirective.as.replace('@', ''); - } else if (typeof importedDirective === 'string') { - originalDirectiveNames[importedDirective.replace('@', '')] = importedDirective.replace('@', ''); + addSubgraphField({ + field, + type: subgraphType, + subgraph, isShareable, + joinFieldArgs, + costSpec: args.costSpec + }); } } } } - - return originalDirectiveNames; } function extractInputObjContent(args: ExtractArguments, info: TypeInfo[]) { const fieldDirective = args.joinSpec.fieldDirective(args.supergraph); - const originalDirectiveNames = args.originalDirectiveNames; for (const { type, subgraphsInfo } of info) { for (const field of type.fields()) { @@ -537,19 +500,30 @@ function extractInputObjContent(args: ExtractArguments, info: TypeInfo[]) { // This was added in join 0.3, so it can genuinely be undefined. const enumValueDirective = args.joinSpec.enumValueDirective(args.supergraph); - const originalDirectiveNames = args.originalDirectiveNames; for (const { type, subgraphsInfo } of info) { - for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) { - propagateDemandControlDirectives(type, subgraphType, subgraph, originalDirectiveNames); + if (args.costSpec) { + for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) { + propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec); + } } for (const value of type.values) { @@ -678,20 +653,20 @@ function maybeDumpSubgraphSchema(subgraph: Subgraph): string { } } -function propagateDemandControlDirectives(source: SchemaElement, dest: SchemaElement, subgraph: Subgraph, originalDirectiveNames?: Record) { - const costDirectiveName = originalDirectiveNames?.[FederationDirectiveName.COST]; - if (costDirectiveName) { - const costDirective = source.appliedDirectivesOf(costDirectiveName).pop(); - if (costDirective) { - dest.applyDirective(subgraph.metadata().costDirective().name, costDirective.arguments()); +function propagateDemandControlDirectives(source: SchemaElement, dest: SchemaElement, subgraph: Subgraph, costSpec: CostSpecDefinition) { + const costDirective = costSpec.costDirective(source.schema()); + if (costDirective) { + const application = source.appliedDirectivesOf(costDirective)[0]; + if (application) { + dest.applyDirective(subgraph.metadata().costDirective().name, application.arguments()); } } - const listSizeDirectiveName = originalDirectiveNames?.[FederationDirectiveName.LIST_SIZE]; - if (listSizeDirectiveName) { - const listSizeDirective = source.appliedDirectivesOf(listSizeDirectiveName).pop(); - if (listSizeDirective) { - dest.applyDirective(subgraph.metadata().listSizeDirective().name, listSizeDirective.arguments()); + const listSizeDirective = costSpec.listSizeDirective(source.schema()); + if (listSizeDirective) { + const application = source.appliedDirectivesOf(listSizeDirective)[0]; + if (application) { + dest.applyDirective(subgraph.metadata().listSizeDirective().name, application.arguments()); } } } @@ -707,14 +682,14 @@ function addSubgraphField({ subgraph, isShareable, joinFieldArgs, - originalDirectiveNames, + costSpec, }: { field: FieldDefinition, type: ObjectType | InterfaceType, subgraph: Subgraph, isShareable: boolean, joinFieldArgs?: JoinFieldDirectiveArguments, - originalDirectiveNames?: Record, + costSpec?: CostSpecDefinition, }): FieldDefinition { const copiedFieldType = joinFieldArgs?.type ? decodeType(joinFieldArgs.type, subgraph.schema, subgraph.name) @@ -723,7 +698,9 @@ function addSubgraphField({ const subgraphField = type.addField(field.name, copiedFieldType); for (const arg of field.arguments()) { const argDef = subgraphField.addArgument(arg.name, copyType(arg.type!, subgraph.schema, subgraph.name), arg.defaultValue); - propagateDemandControlDirectives(arg, argDef, subgraph, originalDirectiveNames) + if (costSpec) { + propagateDemandControlDirectives(arg, argDef, subgraph, costSpec); + } } if (joinFieldArgs?.requires) { subgraphField.applyDirective(subgraph.metadata().requiresDirective(), {'fields': joinFieldArgs.requires}); @@ -769,7 +746,9 @@ function addSubgraphField({ subgraphField.applyDirective(subgraph.metadata().shareableDirective()); } - propagateDemandControlDirectives(field, subgraphField, subgraph, originalDirectiveNames); + if (costSpec) { + propagateDemandControlDirectives(field, subgraphField, subgraph, costSpec); + } return subgraphField; } @@ -779,13 +758,13 @@ function addSubgraphInputField({ type, subgraph, joinFieldArgs, - originalDirectiveNames, + costSpec, }: { field: InputFieldDefinition, type: InputObjectType, subgraph: Subgraph, joinFieldArgs?: JoinFieldDirectiveArguments, - originalDirectiveNames?: Record + costSpec?: CostSpecDefinition, }): InputFieldDefinition { const copiedType = joinFieldArgs?.type ? decodeType(joinFieldArgs?.type, subgraph.schema, subgraph.name) @@ -794,7 +773,9 @@ function addSubgraphInputField({ const inputField = type.addField(field.name, copiedType); inputField.defaultValue = field.defaultValue - propagateDemandControlDirectives(field, inputField, subgraph, originalDirectiveNames); + if (costSpec) { + propagateDemandControlDirectives(field, inputField, subgraph, costSpec); + } return inputField; } diff --git a/internals-js/src/specs/costSpec.ts b/internals-js/src/specs/costSpec.ts index f6f1bda54..ff15c9aa5 100644 --- a/internals-js/src/specs/costSpec.ts +++ b/internals-js/src/specs/costSpec.ts @@ -1,7 +1,7 @@ import { DirectiveLocation } from 'graphql'; import { createDirectiveSpecification } from '../directiveAndTypeSpecification'; import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from './coreSpec'; -import { ListType, NonNullType } from '../definitions'; +import { DirectiveDefinition, ListType, NonNullType, Schema } from '../definitions'; import { registerKnownFeature } from '../knownCoreFeatures'; import { ARGUMENT_COMPOSITION_STRATEGIES } from '../argumentCompositionStrategies'; @@ -41,6 +41,14 @@ export class CostSpecDefinition extends FeatureDefinition { supergraphSpecification: (fedVersion) => COST_VERSIONS.getMinimumRequiredVersion(fedVersion) })); } + + costDirective(schema: Schema): DirectiveDefinition | undefined { + return this.directive(schema, 'cost'); + } + + listSizeDirective(schema: Schema): DirectiveDefinition | undefined { + return this.directive(schema, 'listSize'); + } } export const COST_VERSIONS = new FeatureDefinitions(costIdentity) diff --git a/internals-js/src/supergraphs.ts b/internals-js/src/supergraphs.ts index 3f37a103a..da4d52751 100644 --- a/internals-js/src/supergraphs.ts +++ b/internals-js/src/supergraphs.ts @@ -1,7 +1,9 @@ import { DocumentNode, GraphQLError } from "graphql"; -import { ErrCoreCheckFailed, FeatureUrl, FeatureVersion } from "./specs/coreSpec"; import { CoreFeatures, Schema, sourceASTs } from "./definitions"; +import { ErrCoreCheckFailed, FeatureUrl, FeatureVersion } from "./specs/coreSpec"; import { joinIdentity, JoinSpecDefinition, JOIN_VERSIONS } from "./specs/joinSpec"; +import { CONTEXT_VERSIONS, ContextSpecDefinition } from "./specs/contextSpec"; +import { COST_VERSIONS, costIdentity, CostSpecDefinition } from "./specs/costSpec"; import { buildSchema, buildSchemaFromAST } from "./buildSchema"; import { extractSubgraphsNamesAndUrlsFromSupergraph, extractSubgraphsFromSupergraph } from "./extractSubgraphsFromSupergraph"; import { ERRORS } from "./error"; @@ -81,11 +83,17 @@ function checkFeatureSupport(coreFeatures: CoreFeatures, supportedFeatures: Set< } } -export function validateSupergraph(supergraph: Schema): [CoreFeatures, JoinSpecDefinition] { +export function validateSupergraph(supergraph: Schema): [ + CoreFeatures, + JoinSpecDefinition, + ContextSpecDefinition | undefined, + CostSpecDefinition | undefined, +] { const coreFeatures = supergraph.coreFeatures; if (!coreFeatures) { throw ERRORS.INVALID_FEDERATION_SUPERGRAPH.err("Invalid supergraph: must be a core schema"); } + const joinFeature = coreFeatures.getByIdentity(joinIdentity); if (!joinFeature) { throw ERRORS.INVALID_FEDERATION_SUPERGRAPH.err("Invalid supergraph: must use the join spec"); @@ -95,7 +103,27 @@ export function validateSupergraph(supergraph: Schema): [CoreFeatures, JoinSpecD throw ERRORS.INVALID_FEDERATION_SUPERGRAPH.err( `Invalid supergraph: uses unsupported join spec version ${joinFeature.url.version} (supported versions: ${JOIN_VERSIONS.versions().join(', ')})`); } - return [coreFeatures, joinSpec]; + + const contextFeature = coreFeatures.getByIdentity(ContextSpecDefinition.identity); + let contextSpec = undefined; + if (contextFeature) { + contextSpec = CONTEXT_VERSIONS.find(contextFeature.url.version); + if (!contextSpec) { + throw ERRORS.INVALID_FEDERATION_SUPERGRAPH.err( + `Invalid supergraph: uses unsupported context spec version ${contextFeature.url.version} (supported versions: ${CONTEXT_VERSIONS.versions().join(', ')})`); + } + } + + const costFeature = coreFeatures.getByIdentity(costIdentity); + let costSpec = undefined; + if (costFeature) { + costSpec = COST_VERSIONS.find(costFeature.url.version); + if (!costSpec) { + throw ERRORS.INVALID_FEDERATION_SUPERGRAPH.err( + `Invalid supergraph: uses unsupported cost spec version ${costFeature.url.version} (supported versions: ${COST_VERSIONS.versions().join(', ')})`); + } + } + return [coreFeatures, joinSpec, contextSpec, costSpec]; } export function isFed1Supergraph(supergraph: Schema): boolean { From fd2db6fd808758453430ef770088553dba8d2f24 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Fri, 13 Sep 2024 11:09:03 -0700 Subject: [PATCH 03/10] Revert "Ignore non-resolvable keys in locally satisfiable key checking (#3137)" (#3144) It turns out #3137 needs a few other changes, and we can't ship those in a patch, so we're reverting the PR for now. --- .changeset/rich-tips-study.md | 6 -- query-graphs-js/src/graphPath.ts | 3 - .../src/__tests__/buildPlan.test.ts | 91 ------------------- 3 files changed, 100 deletions(-) delete mode 100644 .changeset/rich-tips-study.md diff --git a/.changeset/rich-tips-study.md b/.changeset/rich-tips-study.md deleted file mode 100644 index 1bc53bdc6..000000000 --- a/.changeset/rich-tips-study.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@apollo/query-planner": patch -"@apollo/query-graphs": patch ---- - -Ignore non-resolvable keys when adding a subgraph jump for `@requires`/`@fromContext`. diff --git a/query-graphs-js/src/graphPath.ts b/query-graphs-js/src/graphPath.ts index 610065d2a..e37751028 100644 --- a/query-graphs-js/src/graphPath.ts +++ b/query-graphs-js/src/graphPath.ts @@ -1930,9 +1930,6 @@ export function getLocallySatisfiableKey(graph: QueryGraph, typeVertex: Vertex): assert(metadata, () => `Could not find federation metadata for source ${typeVertex.source}`); const keyDirective = metadata.keyDirective(); for (const key of type.appliedDirectivesOf(keyDirective)) { - if (!(key.arguments().resolvable ?? true)) { - continue; - } const selection = parseFieldSetArgument({ parentType: type, directive: key }); if (!metadata.selectionSelectsAnyExternalField(selection)) { return selection; diff --git a/query-planner-js/src/__tests__/buildPlan.test.ts b/query-planner-js/src/__tests__/buildPlan.test.ts index 2f59fbdd6..af995c066 100644 --- a/query-planner-js/src/__tests__/buildPlan.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.test.ts @@ -2986,97 +2986,6 @@ describe('@requires', () => { } `); }); - - it('ignores non-resolvable keys when inserting self jump for @requires', () => { - const subgraph1 = { - name: 'A', - typeDefs: gql` - type Query { - t: T - } - - type T @key(fields: "id1", resolvable: false) @key(fields: "id2 id3") { - id1: ID! - id2: ID! - id3: ID! - req: Int @external - v: Int @requires(fields: "req") - } - `, - }; - - const subgraph2 = { - name: 'B', - typeDefs: gql` - type T @key(fields: "id1") { - id1: ID! - req: Int - } - `, - }; - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument( - api, - gql` - { - t { - v - } - } - `, - ); - - const plan = queryPlanner.buildQueryPlan(operation); - expect(plan).toMatchInlineSnapshot(` - QueryPlan { - Sequence { - Fetch(service: "A") { - { - t { - __typename - id1 - id2 - id3 - } - } - }, - Flatten(path: "t") { - Fetch(service: "B") { - { - ... on T { - __typename - id1 - } - } => - { - ... on T { - req - } - } - }, - }, - Flatten(path: "t") { - Fetch(service: "A") { - { - ... on T { - __typename - req - id2 - id3 - } - } => - { - ... on T { - v - } - } - }, - }, - }, - } - `); - }); }); describe('fetch operation names', () => { From e6c05b6c96023aa3dec79889431f8217fcb3806d Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Fri, 13 Sep 2024 17:02:21 -0700 Subject: [PATCH 04/10] Fix value comparison, argument merging, and link feature addition/extraction in composition (#3134) While reviewing recent code changes that released in `2.9.0`, I noticed a few bugs in the code, so I've pushed some fixes for them in this PR (along with some fixes for more ancient bugs). I'll comment on the code with more details, but to summarize the fixes: - Value comparison/default value application has been updated to handle `null`s/`undefined`s better. - Composition argument strategy code has been refactored to handle nullability more cleanly (and fix some bugs around nullability). - Note I've commented out the unused `SUM` strategy, as using it currently wouldn't yield expected results since directive applications are deduped before being merged. - The logic that applies `@link` applications to the supergraph schema for supergraph specs needed by subgraph directives has been updated to handle specs that have multiple directives that need composing (the behavior before was adding spurious directive definitions to the supergraph schema). - Some ancient logic around computing directive/type names for `@core`/`@link` was off. --- .changeset/proud-days-press.md | 7 + ...e.directiveArgumentMergeStrategies.test.ts | 26 +-- composition-js/src/merging/merge.ts | 188 ++++++++++++------ .../__tests__/gateway/lifecycle-hooks.test.ts | 2 +- internals-js/src/__tests__/values.test.ts | 1 + .../src/argumentCompositionStrategies.ts | 118 ++++++----- internals-js/src/definitions.ts | 34 +++- internals-js/src/specs/coreSpec.ts | 53 ++--- internals-js/src/values.ts | 18 +- 9 files changed, 287 insertions(+), 160 deletions(-) create mode 100644 .changeset/proud-days-press.md diff --git a/.changeset/proud-days-press.md b/.changeset/proud-days-press.md new file mode 100644 index 000000000..8330cbf9a --- /dev/null +++ b/.changeset/proud-days-press.md @@ -0,0 +1,7 @@ +--- +"@apollo/federation-internals": patch +"@apollo/gateway": patch +"@apollo/composition": patch +--- + +Fix bugs in composition when merging nulls in directive applications and when handling renames. diff --git a/composition-js/src/__tests__/compose.directiveArgumentMergeStrategies.test.ts b/composition-js/src/__tests__/compose.directiveArgumentMergeStrategies.test.ts index b1be5c54c..c83466e93 100644 --- a/composition-js/src/__tests__/compose.directiveArgumentMergeStrategies.test.ts +++ b/composition-js/src/__tests__/compose.directiveArgumentMergeStrategies.test.ts @@ -89,17 +89,19 @@ describe('composition of directive with non-trivial argument strategies', () => t: 2, k: 1, b: 4, }, }, { - name: 'sum', - type: (schema: Schema) => new NonNullType(schema.intType()), - compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.SUM, - argValues: { - s1: { t: 3, k: 1 }, - s2: { t: 2, k: 5, b: 4 }, - }, - resultValues: { - t: 5, k: 6, b: 4, - }, - }, { + // NOTE: See the note for the SUM strategy in argumentCompositionStrategies.ts + // for more information on why this is commented out. + // name: 'sum', + // type: (schema: Schema) => new NonNullType(schema.intType()), + // compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.SUM, + // argValues: { + // s1: { t: 3, k: 1 }, + // s2: { t: 2, k: 5, b: 4 }, + // }, + // resultValues: { + // t: 5, k: 6, b: 4, + // }, + // }, { name: 'intersection', type: (schema: Schema) => new NonNullType(new ListType(new NonNullType(schema.stringType()))), compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.INTERSECTION, @@ -219,7 +221,7 @@ describe('composition of directive with non-trivial argument strategies', () => const s = result.schema; expect(directiveStrings(s.schemaDefinition, name)).toStrictEqual([ - `@link(url: "https://specs.apollo.dev/${name}/v0.1", import: ["@${name}"])` + `@link(url: "https://specs.apollo.dev/${name}/v0.1")` ]); const t = s.type('T') as ObjectType; diff --git a/composition-js/src/merging/merge.ts b/composition-js/src/merging/merge.ts index 294e54cdc..01b3430dd 100644 --- a/composition-js/src/merging/merge.ts +++ b/composition-js/src/merging/merge.ts @@ -31,7 +31,6 @@ import { CompositeType, Subgraphs, JOIN_VERSIONS, - INACCESSIBLE_VERSIONS, NamedSchemaElement, errorCauses, isObjectType, @@ -69,7 +68,6 @@ import { CoreSpecDefinition, FeatureVersion, FEDERATION_VERSIONS, - InaccessibleSpecDefinition, LinkDirectiveArgs, sourceIdentity, FeatureUrl, @@ -81,6 +79,10 @@ import { isNullableType, isFieldDefinition, Post20FederationDirectiveDefinition, + DirectiveCompositionSpecification, + FeatureDefinition, + CoreImport, + inaccessibleIdentity, } from "@apollo/federation-internals"; import { ASTNode, GraphQLError, DirectiveLocation } from "graphql"; import { @@ -345,6 +347,12 @@ interface OverrideArgs { label?: string; } +interface MergedDirectiveInfo { + definition: DirectiveDefinition; + argumentsMerger?: ArgumentMerger; + staticArgumentTransform?: StaticArgumentsTransform; +} + class Merger { readonly names: readonly string[]; readonly subgraphsSchema: readonly Schema[]; @@ -353,7 +361,8 @@ class Merger { readonly merged: Schema = new Schema(); readonly subgraphNamesToJoinSpecName: Map; readonly mergedFederationDirectiveNames = new Set(); - readonly mergedFederationDirectiveInSupergraph = new Map(); + readonly mergedFederationDirectiveInSupergraphByDirectiveName = + new Map(); readonly enumUsages = new Map(); private composeDirectiveManager: ComposeDirectiveManager; private mismatchReporter: MismatchReporter; @@ -364,7 +373,7 @@ class Merger { }[]; private joinSpec: JoinSpecDefinition; private linkSpec: CoreSpecDefinition; - private inaccessibleSpec: InaccessibleSpecDefinition; + private inaccessibleDirectiveInSupergraph?: DirectiveDefinition; private latestFedVersionUsed: FeatureVersion; private joinDirectiveIdentityURLs = new Set(); private schemaToImportNameToFeatureUrl = new Map>(); @@ -375,7 +384,6 @@ class Merger { this.latestFedVersionUsed = this.getLatestFederationVersionUsed(); this.joinSpec = JOIN_VERSIONS.getMinimumRequiredVersion(this.latestFedVersionUsed); this.linkSpec = LINK_VERSIONS.getMinimumRequiredVersion(this.latestFedVersionUsed); - this.inaccessibleSpec = INACCESSIBLE_VERSIONS.getMinimumRequiredVersion(this.latestFedVersionUsed); this.fieldsWithFromContext = this.getFieldsWithFromContextDirective(); this.fieldsWithOverride = this.getFieldsWithOverrideDirective(); @@ -470,59 +478,127 @@ class Merger { assert(errors.length === 0, "We shouldn't have errors adding the join spec to the (still empty) supergraph schema"); const directivesMergeInfo = collectCoreDirectivesToCompose(this.subgraphs); - for (const mergeInfo of directivesMergeInfo) { - this.validateAndMaybeAddSpec(mergeInfo); - } - + this.validateAndMaybeAddSpecs(directivesMergeInfo); return this.joinSpec.populateGraphEnum(this.merged, this.subgraphs); } - private validateAndMaybeAddSpec({url, name, definitionsPerSubgraph, compositionSpec}: CoreDirectiveInSubgraphs) { - // Not composition specification means that it shouldn't be composed. - if (!compositionSpec) { - return; - } - - let nameInSupergraph: string | undefined; - for (const subgraph of this.subgraphs) { - const directive = definitionsPerSubgraph.get(subgraph.name); - if (!directive) { - continue; + private validateAndMaybeAddSpecs(directivesMergeInfo: CoreDirectiveInSubgraphs[]) { + const supergraphInfoByIdentity = new Map< + string, + { + specInSupergraph: FeatureDefinition; + directives: { + nameInFeature: string; + nameInSupergraph: string; + compositionSpec: DirectiveCompositionSpecification; + }[]; } + >; - if (!nameInSupergraph) { - nameInSupergraph = directive.name; - } else if (nameInSupergraph !== directive.name) { - this.mismatchReporter.reportMismatchError( - ERRORS.LINK_IMPORT_NAME_MISMATCH, - `The "@${name}" directive (from ${url}) is imported with mismatched name between subgraphs: it is imported as `, - directive, - sourcesFromArray(this.subgraphs.values().map((s) => definitionsPerSubgraph.get(s.name))), - (def) => `"@${def.name}"`, - ); + for (const {url, name, definitionsPerSubgraph, compositionSpec} of directivesMergeInfo) { + // No composition specification means that it shouldn't be composed. + if (!compositionSpec) { return; } + + let nameInSupergraph: string | undefined; + for (const subgraph of this.subgraphs) { + const directive = definitionsPerSubgraph.get(subgraph.name); + if (!directive) { + continue; + } + + if (!nameInSupergraph) { + nameInSupergraph = directive.name; + } else if (nameInSupergraph !== directive.name) { + this.mismatchReporter.reportMismatchError( + ERRORS.LINK_IMPORT_NAME_MISMATCH, + `The "@${name}" directive (from ${url}) is imported with mismatched name between subgraphs: it is imported as `, + directive, + sourcesFromArray(this.subgraphs.values().map((s) => definitionsPerSubgraph.get(s.name))), + (def) => `"@${def.name}"`, + ); + return; + } + } + + // If we get here with `nameInSupergraph` unset, it means there is no usage for the directive at all and we + // don't bother adding the spec to the supergraph. + if (nameInSupergraph) { + const specInSupergraph = compositionSpec.supergraphSpecification(this.latestFedVersionUsed); + let supergraphInfo = supergraphInfoByIdentity.get(specInSupergraph.url.identity); + if (supergraphInfo) { + assert( + specInSupergraph.url.equals(supergraphInfo.specInSupergraph.url), + `Spec ${specInSupergraph.url} directives disagree on version for supergraph`, + ); + } else { + supergraphInfo = { + specInSupergraph, + directives: [], + }; + supergraphInfoByIdentity.set(specInSupergraph.url.identity, supergraphInfo); + } + supergraphInfo.directives.push({ + nameInFeature: name, + nameInSupergraph, + compositionSpec, + }); + } } - // If we get here with `nameInSupergraph` unset, it means there is no usage for the directive at all and we - // don't bother adding the spec to the supergraph. - if (nameInSupergraph) { - const specInSupergraph = compositionSpec.supergraphSpecification(this.latestFedVersionUsed); - const errors = this.linkSpec.applyFeatureAsLink(this.merged, specInSupergraph, specInSupergraph.defaultCorePurpose, [{ name, as: name === nameInSupergraph ? undefined : nameInSupergraph }], ); - assert(errors.length === 0, "We shouldn't have errors adding the join spec to the (still empty) supergraph schema"); - const feature = this.merged?.coreFeatures?.getByIdentity(specInSupergraph.url.identity); + for (const { specInSupergraph, directives } of supergraphInfoByIdentity.values()) { + const imports: CoreImport[] = []; + for (const { nameInFeature, nameInSupergraph } of directives) { + const defaultNameInSupergraph = CoreFeature.directiveNameInSchemaForCoreArguments( + specInSupergraph.url, + specInSupergraph.url.name, + [], + nameInFeature, + ); + if (nameInSupergraph !== defaultNameInSupergraph) { + imports.push(nameInFeature === nameInSupergraph + ? { name: `@${nameInFeature}` } + : { name: `@${nameInFeature}`, as: `@${nameInSupergraph}` } + ); + } + } + const errors = this.linkSpec.applyFeatureToSchema( + this.merged, + specInSupergraph, + undefined, + specInSupergraph.defaultCorePurpose, + imports, + ); + assert( + errors.length === 0, + "We shouldn't have errors adding the join spec to the (still empty) supergraph schema" + ); + const feature = this.merged.coreFeatures?.getByIdentity(specInSupergraph.url.identity); assert(feature, 'Should have found the feature we just added'); - const argumentsMerger = compositionSpec.argumentsMerger?.call(null, this.merged, feature); - if (argumentsMerger instanceof GraphQLError) { - // That would mean we made a mistake in the declaration of a hard-coded directive, so we just throw right away so this can be caught and corrected. - throw argumentsMerger; - } - this.mergedFederationDirectiveNames.add(nameInSupergraph); - this.mergedFederationDirectiveInSupergraph.set(name, { - definition: this.merged.directive(nameInSupergraph)!, - argumentsMerger, - staticArgumentTransform: compositionSpec.staticArgumentTransform, - }); + for (const { nameInFeature, nameInSupergraph, compositionSpec } of directives) { + const argumentsMerger = compositionSpec.argumentsMerger?.call(null, this.merged, feature); + if (argumentsMerger instanceof GraphQLError) { + // That would mean we made a mistake in the declaration of a hard-coded directive, + // so we just throw right away so this can be caught and corrected. + throw argumentsMerger; + } + this.mergedFederationDirectiveNames.add(nameInSupergraph); + this.mergedFederationDirectiveInSupergraphByDirectiveName.set(nameInSupergraph, { + definition: this.merged.directive(nameInSupergraph)!, + argumentsMerger, + staticArgumentTransform: compositionSpec.staticArgumentTransform, + }); + // If we encounter the @inaccessible directive, we need to record its + // definition so certain merge validations that care about @inaccessible + // can act accordingly. + if ( + specInSupergraph.identity === inaccessibleIdentity + && nameInFeature === specInSupergraph.url.name + ) { + this.inaccessibleDirectiveInSupergraph = this.merged.directive(nameInSupergraph)!; + } + } } } @@ -2464,8 +2540,8 @@ class Merger { this.recordAppliedDirectivesToMerge(valueSources, value); this.addJoinEnumValue(valueSources, value); - const inaccessibleInSupergraph = this.mergedFederationDirectiveInSupergraph.get(this.inaccessibleSpec.inaccessibleDirectiveSpec.name); - const isInaccessible = inaccessibleInSupergraph && value.hasAppliedDirective(inaccessibleInSupergraph.definition); + const isInaccessible = this.inaccessibleDirectiveInSupergraph + && value.hasAppliedDirective(this.inaccessibleDirectiveInSupergraph); // The merging strategy depends on the enum type usage: // - if it is _only_ used in position of Input type, we merge it with an "intersection" strategy (like other input types/things). // - if it is _only_ used in position of Output type, we merge it with an "union" strategy (like other output types/things). @@ -2562,8 +2638,6 @@ class Merger { } private mergeInput(inputSources: Sources, dest: InputObjectType) { - const inaccessibleInSupergraph = this.mergedFederationDirectiveInSupergraph.get(this.inaccessibleSpec.inaccessibleDirectiveSpec.name); - // Like for other inputs, we add all the fields found in any subgraphs initially as a simple mean to have a complete list of // field to iterate over, but we will remove those that are not in all subgraphs. const added = this.addFieldsShallow(inputSources, dest); @@ -2572,7 +2646,8 @@ class Merger { // compatibility between definitions and 2) we actually want to see if the result is marked inaccessible or not and it makes // that easier. this.mergeInputField(subgraphFields, destField); - const isInaccessible = inaccessibleInSupergraph && destField.hasAppliedDirective(inaccessibleInSupergraph.definition); + const isInaccessible = this.inaccessibleDirectiveInSupergraph + && destField.hasAppliedDirective(this.inaccessibleDirectiveInSupergraph); // Note that if the field is manually marked @inaccessible, we can always accept it to be inconsistent between subgraphs since // it won't be exposed in the API, and we don't hint about it because we're just doing what the user is explicitely asking. if (!isInaccessible && someSources(subgraphFields, field => !field)) { @@ -2840,8 +2915,7 @@ class Merger { // is @inaccessible, which is necessary to exist in the supergraph for EnumValues to properly // determine whether the fact that a value is both input / output will matter private recordAppliedDirectivesToMerge(sources: Sources>, dest: SchemaElement) { - const inaccessibleInSupergraph = this.mergedFederationDirectiveInSupergraph.get(this.inaccessibleSpec.inaccessibleDirectiveSpec.name); - const inaccessibleName = inaccessibleInSupergraph?.definition.name; + const inaccessibleName = this.inaccessibleDirectiveInSupergraph?.name; const names = this.gatherAppliedDirectiveNames(sources); if (inaccessibleName && names.has(inaccessibleName)) { @@ -2905,7 +2979,7 @@ class Merger { return; } - const directiveInSupergraph = this.mergedFederationDirectiveInSupergraph.get(name); + const directiveInSupergraph = this.mergedFederationDirectiveInSupergraphByDirectiveName.get(name); if (dest.schema().directive(name)?.repeatable) { // For repeatable directives, we simply include each application found but with exact duplicates removed @@ -2945,7 +3019,7 @@ class Merger { if (differentApplications.length === 1) { dest.applyDirective(name, differentApplications[0].arguments(false)); } else { - const info = this.mergedFederationDirectiveInSupergraph.get(name); + const info = this.mergedFederationDirectiveInSupergraphByDirectiveName.get(name); if (info && info.argumentsMerger) { const mergedArguments = Object.create(null); const applicationsArguments = differentApplications.map((a) => a.arguments(true)); diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index 458175876..43b65e309 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -149,7 +149,7 @@ describe('lifecycle hooks', () => { // the supergraph (even just formatting differences), this ID will change // and this test will have to updated. expect(secondCall[0]!.compositionId).toMatchInlineSnapshot( - `"4aa2278e35df345ff5959a30546d2e9ef9e997204b4ffee4a42344b578b36068"`, + `"6dc1bde2b9818fabec62208c5d8825abaa1bae89635fa6f3a5ffea7b78fc6d82"`, ); // second call should have previous info in the second arg expect(secondCall[1]!.compositionId).toEqual(expectedFirstId); diff --git a/internals-js/src/__tests__/values.test.ts b/internals-js/src/__tests__/values.test.ts index eaab39aa7..6e1d167fb 100644 --- a/internals-js/src/__tests__/values.test.ts +++ b/internals-js/src/__tests__/values.test.ts @@ -414,5 +414,6 @@ describe('objectEquals tests', () => { expect(valueEquals({ foo: 'foo', bar: undefined }, { foo: 'foo' })).toBe( false, ); + expect(valueEquals({}, null)).toBe(false); }); }); diff --git a/internals-js/src/argumentCompositionStrategies.ts b/internals-js/src/argumentCompositionStrategies.ts index 85a870773..f9d60ef3f 100644 --- a/internals-js/src/argumentCompositionStrategies.ts +++ b/internals-js/src/argumentCompositionStrategies.ts @@ -13,87 +13,105 @@ export type ArgumentCompositionStrategy = { function supportFixedTypes(types: (schema: Schema) => InputType[]): TypeSupportValidator { return (schema, type) => { const supported = types(schema); - if (!supported.some((t) => sameType(t, type))) { - return { valid: false, supportedMsg: `type(s) ${supported.join(', ')}` }; - } - return { valid: true }; + return supported.some((t) => sameType(t, type)) + ? { valid: true } + : { valid: false, supportedMsg: `type(s) ${supported.join(', ')}` }; }; } function supportAnyNonNullArray(): TypeSupportValidator { - return (_, type) => { - if (!isNonNullType(type) || !isListType(type.ofType)) { - return { valid: false, supportedMsg: 'non nullable list types of any type'}; - } - return { valid: true }; + return (_, type) => isNonNullType(type) && isListType(type.ofType) + ? { valid: true } + : { valid: false, supportedMsg: 'non nullable list types of any type' } +} + +function supportAnyArray(): TypeSupportValidator { + return (_, type) => isListType(type) || (isNonNullType(type) && isListType(type.ofType)) + ? { valid: true } + : { valid: false, supportedMsg: 'list types of any type' }; +} + +// NOTE: This function makes the assumption that for the directive argument +// being merged, it is not "nullable with non-null default" in the supergraph +// schema (this kind of type/default combo is confusing and should be avoided, +// if possible). This assumption allows this function to replace null with +// undefined, which makes for a cleaner supergraph schema. +function mergeNullableValues( + mergeValues: (values: T[]) => T +): (values: (T | null | undefined)[]) => T | undefined { + return (values: (T | null | undefined)[]) => { + const nonNullValues = values.filter((v) => v !== null && v !== undefined) as T[]; + return nonNullValues.length > 0 + ? mergeValues(nonNullValues) + : undefined; }; } +function unionValues(values: any[]): any { + return values.reduce((acc, next) => { + const newValues = next.filter((v1: any) => !acc.some((v2: any) => valueEquals(v1, v2))); + return acc.concat(newValues); + }, []); +} + export const ARGUMENT_COMPOSITION_STRATEGIES = { MAX: { name: 'MAX', isTypeSupported: supportFixedTypes((schema: Schema) => [new NonNullType(schema.intType())]), - mergeValues: (values: any[]) => Math.max(...values), + mergeValues: (values: number[]) => Math.max(...values), }, MIN: { name: 'MIN', isTypeSupported: supportFixedTypes((schema: Schema) => [new NonNullType(schema.intType())]), - mergeValues: (values: any[]) => Math.min(...values), - }, - SUM: { - name: 'SUM', - isTypeSupported: supportFixedTypes((schema: Schema) => [new NonNullType(schema.intType())]), - mergeValues: (values: any[]) => values.reduce((acc, val) => acc + val, 0), + mergeValues: (values: number[]) => Math.min(...values), }, + // NOTE: This doesn't work today because directive applications are de-duped + // before being merged, we'd need to modify merge logic if we need this kind + // of behavior. + // SUM: { + // name: 'SUM', + // isTypeSupported: supportFixedTypes((schema: Schema) => [new NonNullType(schema.intType())]), + // mergeValues: (values: any[]) => values.reduce((acc, val) => acc + val, 0), + // }, INTERSECTION: { name: 'INTERSECTION', isTypeSupported: supportAnyNonNullArray(), - mergeValues: (values: any[]) => values.reduce((acc, val) => acc.filter((v1: any) => val.some((v2: any) => valueEquals(v1, v2))), values[0]), + mergeValues: (values: any[]) => values.reduce((acc, next) => { + if (acc === undefined) { + return next; + } else { + return acc.filter((v1: any) => next.some((v2: any) => valueEquals(v1, v2))); + } + }, undefined) ?? [], }, UNION: { name: 'UNION', isTypeSupported: supportAnyNonNullArray(), - mergeValues: (values: any[]) => - values.reduce((acc, val) => { - const newValues = val.filter((v1: any) => !acc.some((v2: any) => valueEquals(v1, v2))); - return acc.concat(newValues); - }, []), + mergeValues: unionValues, }, NULLABLE_AND: { name: 'NULLABLE_AND', - isTypeSupported: supportFixedTypes((schema: Schema) => [schema.booleanType()]), - mergeValues: (values: (boolean | null | undefined)[]) => values.reduce((acc, next) => { - if (acc === null || acc === undefined) { - return next; - } else if (next === null || next === undefined) { - return acc; - } else { - return acc && next; - } - }, undefined), + isTypeSupported: supportFixedTypes((schema: Schema) => [ + schema.booleanType(), + new NonNullType(schema.booleanType()) + ]), + mergeValues: mergeNullableValues( + (values: boolean[]) => values.every((v) => v) + ), }, NULLABLE_MAX: { name: 'NULLABLE_MAX', - isTypeSupported: supportFixedTypes((schema: Schema) => [schema.intType(), new NonNullType(schema.intType())]), - mergeValues: (values: any[]) => values.reduce((a: any, b: any) => a !== undefined && b !== undefined ? Math.max(a, b) : a ?? b, undefined), + isTypeSupported: supportFixedTypes((schema: Schema) => [ + schema.intType(), + new NonNullType(schema.intType()) + ]), + mergeValues: mergeNullableValues( + (values: number[]) => Math.max(...values) + ) }, NULLABLE_UNION: { name: 'NULLABLE_UNION', - isTypeSupported: (_: Schema, type: InputType) => ({ valid: isListType(type) }), - mergeValues: (values: any[]) => { - if (values.every((v) => v === undefined)) { - return undefined; - } - - const combined = new Set(); - for (const subgraphValues of values) { - if (Array.isArray(subgraphValues)) { - for (const value of subgraphValues) { - combined.add(value); - } - } - } - return Array.from(combined); - } + isTypeSupported: supportAnyArray(), + mergeValues: mergeNullableValues(unionValues), } } diff --git a/internals-js/src/definitions.ts b/internals-js/src/definitions.ts index e52013047..cdd8e09dd 100644 --- a/internals-js/src/definitions.ts +++ b/internals-js/src/definitions.ts @@ -984,12 +984,28 @@ export class CoreFeature { } directiveNameInSchema(name: string): string { - const elementImport = this.imports.find((i) => i.name.charAt(0) === '@' && i.name.slice(1) === name); + return CoreFeature.directiveNameInSchemaForCoreArguments( + this.url, + this.nameInSchema, + this.imports, + name, + ); + } + + static directiveNameInSchemaForCoreArguments( + specUrl: FeatureUrl, + specNameInSchema: string, + imports: CoreImport[], + directiveNameInSpec: string, + ): string { + const elementImport = imports.find((i) => + i.name.charAt(0) === '@' && i.name.slice(1) === directiveNameInSpec + ); return elementImport - ? (elementImport.as?.slice(1) ?? name) - : (name === this.url.name - ? this.nameInSchema - : this.nameInSchema + '__' + name + ? (elementImport.as?.slice(1) ?? directiveNameInSpec) + : (directiveNameInSpec === specUrl.name + ? specNameInSchema + : specNameInSchema + '__' + directiveNameInSpec ); } @@ -1064,7 +1080,7 @@ export class CoreFeatures { const feature = this.byAlias.get(splitted[0]); return feature ? { feature, - nameInFeature: splitted[1], + nameInFeature: splitted.slice(1).join('__'), isImported: false, } : undefined; } else { @@ -1076,7 +1092,7 @@ export class CoreFeatures { if ((as ?? name) === importName) { return { feature, - nameInFeature: name.slice(1), + nameInFeature: isDirective ? name.slice(1) : name, isImported: true, }; } @@ -1088,8 +1104,8 @@ export class CoreFeatures { if (directFeature && isDirective) { return { feature: directFeature, - nameInFeature: directFeature.imports.find(imp => imp.as === `@${element.name}`)?.name.slice(1) ?? element.name, - isImported: true, + nameInFeature: element.name, + isImported: false, }; } diff --git a/internals-js/src/specs/coreSpec.ts b/internals-js/src/specs/coreSpec.ts index 909769ef4..aaeb155cb 100644 --- a/internals-js/src/specs/coreSpec.ts +++ b/internals-js/src/specs/coreSpec.ts @@ -195,7 +195,8 @@ export type CoreDirectiveArgs = { url: undefined, feature: string, as?: string, - for?: string + for?: string, + import: undefined, } export type LinkDirectiveArgs = { @@ -203,7 +204,7 @@ export type LinkDirectiveArgs = { feature: undefined, as?: string, for?: string, - import?: (string | CoreImport)[] + import?: (string | CoreImport)[], } export type CoreOrLinkDirectiveArgs = CoreDirectiveArgs | LinkDirectiveArgs; @@ -539,36 +540,36 @@ export class CoreSpecDefinition extends FeatureDefinition { return feature.url.version; } - applyFeatureToSchema(schema: Schema, feature: FeatureDefinition, as?: string, purpose?: CorePurpose): GraphQLError[] { + applyFeatureToSchema( + schema: Schema, + feature: FeatureDefinition, + as?: string, + purpose?: CorePurpose, + imports?: CoreImport[], + ): GraphQLError[] { const coreDirective = this.coreDirective(schema); const args = { [this.urlArgName()]: feature.toString(), as, - } as CoreDirectiveArgs; - if (this.supportPurposes() && purpose) { - args.for = purpose; - } - schema.schemaDefinition.applyDirective(coreDirective, args); - return feature.addElementsToSchema(schema); - } - - applyFeatureAsLink(schema: Schema, feature: FeatureDefinition, purpose?: CorePurpose, imports?: CoreImport[]): GraphQLError[] { - const existing = schema.schemaDefinition.appliedDirectivesOf(linkDirectiveDefaultName).find((link) => link.arguments().url === feature.toString()); - if (existing) { - existing.remove(); + } as CoreOrLinkDirectiveArgs; + if (purpose) { + if (this.supportPurposes()) { + args.for = purpose; + } else { + return [new GraphQLError( + `Cannot apply feature ${feature} with purpose since the schema's @core/@link version does not support it.` + )]; + } } - - const coreDirective = this.coreDirective(schema); - const args: LinkDirectiveArgs = { - url: feature.toString(), - import: (existing?.arguments().import ?? []).concat(imports?.map((i) => i.as ? { name: `@${i.name}`, as: `@${i.as}` } : `@${i.name}`)), - feature: undefined, - }; - - if (this.supportPurposes() && purpose) { - args.for = purpose; + if (imports && imports.length > 0) { + if (this.supportImport()) { + args.import = imports.map(i => i.as ? i : i.name); + } else { + return [new GraphQLError( + `Cannot apply feature ${feature} with imports since the schema's @core/@link version does not support it.` + )]; + } } - schema.schemaDefinition.applyDirective(coreDirective, args); return feature.addElementsToSchema(schema); } diff --git a/internals-js/src/values.ts b/internals-js/src/values.ts index 11b2968e5..62de2a08e 100644 --- a/internals-js/src/values.ts +++ b/internals-js/src/values.ts @@ -135,8 +135,10 @@ export function valueEquals(a: any, b: any): boolean { if (Array.isArray(a)) { return Array.isArray(b) && arrayValueEquals(a, b) ; } - if (typeof a === 'object') { - return typeof b === 'object' && objectEquals(a, b); + // Note that typeof null === 'object', so we have to manually rule that out + // here. + if (a !== null && typeof a === 'object') { + return b !== null && typeof b === 'object' && objectEquals(a, b); } return a === b; } @@ -224,8 +226,10 @@ function applyDefaultValues(value: any, type: InputType): any { if (fieldValue === undefined) { if (field.defaultValue !== undefined) { updated[field.name] = applyDefaultValues(field.defaultValue, field.type); - } else if (isNonNullType(field.type)) { - throw ERRORS.INVALID_GRAPHQL.err(`Field "${field.name}" of required type ${type} was not provided.`); + } else if (!isNonNullType(field.type)) { + updated[field.name] = null; + } else { + throw ERRORS.INVALID_GRAPHQL.err(`Required field "${field.name}" of type ${type} was not provided.`); } } else { updated[field.name] = applyDefaultValues(fieldValue, field.type); @@ -249,8 +253,12 @@ export function withDefaultValues(value: any, argument: ArgumentDefinition) throw buildError(`Cannot compute default value for argument ${argument} as the type is undefined`); } if (value === undefined) { - if (argument.defaultValue) { + if (argument.defaultValue !== undefined) { return applyDefaultValues(argument.defaultValue, argument.type); + } else if (!isNonNullType(argument.type)) { + return null; + } else { + throw ERRORS.INVALID_GRAPHQL.err(`Required argument "${argument.coordinate}" was not provided.`); } } return applyDefaultValues(value, argument.type); From fc55ac97d1eb4a158d34424d88f0e342369da14e Mon Sep 17 00:00:00 2001 From: Chris Lenfest Date: Mon, 16 Sep 2024 07:59:22 -0500 Subject: [PATCH 05/10] Relax external field requirements (#3142) Relax error for detecting whether `@external` field matches. If the external field has extra optional parameters, allow it to pass composition as being able to pass the parameter is not necessary for a required field. --- .changeset/slow-cups-exist.md | 5 +++ .../src/__tests__/compose.external.test.ts | 34 ++++++++++++++++++- composition-js/src/merging/merge.ts | 4 ++- 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 .changeset/slow-cups-exist.md diff --git a/.changeset/slow-cups-exist.md b/.changeset/slow-cups-exist.md new file mode 100644 index 000000000..994f3caa0 --- /dev/null +++ b/.changeset/slow-cups-exist.md @@ -0,0 +1,5 @@ +--- +"@apollo/composition": patch +--- + +Relax error for detecting whether `@external` field matches. If the external field has extra optional parameters, allow it to pass composition as being able to pass the parameter is not necessary for a required field. diff --git a/composition-js/src/__tests__/compose.external.test.ts b/composition-js/src/__tests__/compose.external.test.ts index a2b62795c..00a661e4d 100644 --- a/composition-js/src/__tests__/compose.external.test.ts +++ b/composition-js/src/__tests__/compose.external.test.ts @@ -62,7 +62,7 @@ describe('tests related to @external', () => { typeDefs: gql` type T @key(fields: "id") { id: ID! - f(x: Int): String @shareable + f(x: Int!): String @shareable } `, }; @@ -73,6 +73,38 @@ describe('tests related to @external', () => { ['EXTERNAL_ARGUMENT_MISSING', 'Field "T.f" is missing argument "T.f(x:)" in some subgraphs where it is marked @external: argument "T.f(x:)" is declared in subgraph "subgraphB" but not in subgraph "subgraphA" (where "T.f" is @external).'], ]); }); + + it('succeeds on @external definition where difference between definitions is an optional argument', () => { + const subgraphA = { + name: 'subgraphA', + typeDefs: gql` + type Query { + locations: [Location] + } + + type Location @key(fields: "id") { + id: ID! + name: String! + photo(smallVersion: Boolean): String! # New optional arg shouldn't break existing clients + } + `, + }; + + const subgraphB = { + name: 'subgraphB', + typeDefs: gql` + type Location @key(fields: "id") { + id: ID! + photo: String! @external + banner: String @requires(fields: "photo") + } + `, + }; + + const result = composeAsFed2Subgraphs([subgraphA, subgraphB]); + assertCompositionSuccess(result); + }); + it('errors on incompatible argument types in @external declaration', () => { const subgraphA = { diff --git a/composition-js/src/merging/merge.ts b/composition-js/src/merging/merge.ts index 01b3430dd..722e04075 100644 --- a/composition-js/src/merging/merge.ts +++ b/composition-js/src/merging/merge.ts @@ -1864,7 +1864,9 @@ class Merger { const name = destArg.name; const arg = source.argument(name); if (!arg) { - invalidArgsPresence.add(name); + if (!destArg.type || destArg.type.kind === 'NonNullType') { + invalidArgsPresence.add(name); + } continue; } if (!sameType(destArg.type!, arg.type!) && !this.isStrictSubtype(arg.type!, destArg.type!)) { From e7aeb9fad7ab013fcfb77fd9e759101b57170e58 Mon Sep 17 00:00:00 2001 From: Chris Lenfest Date: Mon, 16 Sep 2024 14:24:49 -0500 Subject: [PATCH 06/10] Revert #3142 (#3145) --- .changeset/slow-cups-exist.md | 5 --- .../src/__tests__/compose.external.test.ts | 34 +------------------ composition-js/src/merging/merge.ts | 4 +-- 3 files changed, 2 insertions(+), 41 deletions(-) delete mode 100644 .changeset/slow-cups-exist.md diff --git a/.changeset/slow-cups-exist.md b/.changeset/slow-cups-exist.md deleted file mode 100644 index 994f3caa0..000000000 --- a/.changeset/slow-cups-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/composition": patch ---- - -Relax error for detecting whether `@external` field matches. If the external field has extra optional parameters, allow it to pass composition as being able to pass the parameter is not necessary for a required field. diff --git a/composition-js/src/__tests__/compose.external.test.ts b/composition-js/src/__tests__/compose.external.test.ts index 00a661e4d..a2b62795c 100644 --- a/composition-js/src/__tests__/compose.external.test.ts +++ b/composition-js/src/__tests__/compose.external.test.ts @@ -62,7 +62,7 @@ describe('tests related to @external', () => { typeDefs: gql` type T @key(fields: "id") { id: ID! - f(x: Int!): String @shareable + f(x: Int): String @shareable } `, }; @@ -73,38 +73,6 @@ describe('tests related to @external', () => { ['EXTERNAL_ARGUMENT_MISSING', 'Field "T.f" is missing argument "T.f(x:)" in some subgraphs where it is marked @external: argument "T.f(x:)" is declared in subgraph "subgraphB" but not in subgraph "subgraphA" (where "T.f" is @external).'], ]); }); - - it('succeeds on @external definition where difference between definitions is an optional argument', () => { - const subgraphA = { - name: 'subgraphA', - typeDefs: gql` - type Query { - locations: [Location] - } - - type Location @key(fields: "id") { - id: ID! - name: String! - photo(smallVersion: Boolean): String! # New optional arg shouldn't break existing clients - } - `, - }; - - const subgraphB = { - name: 'subgraphB', - typeDefs: gql` - type Location @key(fields: "id") { - id: ID! - photo: String! @external - banner: String @requires(fields: "photo") - } - `, - }; - - const result = composeAsFed2Subgraphs([subgraphA, subgraphB]); - assertCompositionSuccess(result); - }); - it('errors on incompatible argument types in @external declaration', () => { const subgraphA = { diff --git a/composition-js/src/merging/merge.ts b/composition-js/src/merging/merge.ts index 722e04075..01b3430dd 100644 --- a/composition-js/src/merging/merge.ts +++ b/composition-js/src/merging/merge.ts @@ -1864,9 +1864,7 @@ class Merger { const name = destArg.name; const arg = source.argument(name); if (!arg) { - if (!destArg.type || destArg.type.kind === 'NonNullType') { - invalidArgsPresence.add(name); - } + invalidArgsPresence.add(name); continue; } if (!sameType(destArg.type!, arg.type!) && !this.isStrictSubtype(arg.type!, destArg.type!)) { From 72ebc48e291dd34cd57c98d1af5c783f3997e1fa Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Tue, 17 Sep 2024 14:39:15 -0700 Subject: [PATCH 07/10] Add some tests for bugs fixed by #3134 and #3136 (#3146) The PRs #3134 and #3136 were merged without tests illustrating the bugs they were fixing. This PR adds tests for two of the bugs fixed by them; these tests fail on `2.9.0`, but succeed after the aforementioned PRs. --- .../__tests__/compose.demandControl.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/composition-js/src/__tests__/compose.demandControl.test.ts b/composition-js/src/__tests__/compose.demandControl.test.ts index e2c2279b3..a9dd4333a 100644 --- a/composition-js/src/__tests__/compose.demandControl.test.ts +++ b/composition-js/src/__tests__/compose.demandControl.test.ts @@ -336,6 +336,10 @@ describe('demand control directive composition', () => { assertCompositionSuccess(result); expect(result.hints).toEqual([]); + expect(result.schema.directive(`cost`)).toBeDefined(); + expect(result.schema.directive(`listSize`)).toBeDefined(); + expect(result.schema.directive(`cost__listSize`)).toBeUndefined(); + const costDirectiveApplications = fieldWithCost(result)?.appliedDirectivesOf('cost'); expect(costDirectiveApplications?.toString()).toMatchString(`@cost(weight: 5)`); @@ -773,5 +777,38 @@ describe('demand control directive extraction', () => { } `); }); + + it('does not attempt to extract them to the subgraphs with similar spec URL', () => { + const subgraphA = { + name: 'subgraph-a', + typeDefs: asFed2SubgraphDocument(gql` + extend schema + @link(url: "https://specs.apollo.dev.foo.com/cost/v0.1") + @composeDirective(name: "@cost") + + directive @cost(weight: Int!) on FIELD_DEFINITION + + type Query { + a: Int @cost(weight: 1) + } + `) + }; + + const result = composeServices([subgraphA]); + assertCompositionSuccess(result); + const supergraph = Supergraph.build(result.supergraphSdl); + + expect(supergraph.subgraphs().get(subgraphA.name)?.toString()).toMatchString(` + schema + ${FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS} + { + query: Query + } + + type Query { + a: Int + } + `); + }); }); }); From a7a74c53bb43aad19734c803f41f62f43cb02214 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 17:10:42 -0700 Subject: [PATCH 08/10] release: on branch main (#3139) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @apollo/composition@2.9.1 ### Patch Changes - Fix bugs in composition when merging nulls in directive applications and when handling renames. ([#3134](https://github.com/apollographql/federation/pull/3134)) - Updated dependencies \[[`b8e4ab5352a4dfd262af49493fdd42e86e5e3d99`](https://github.com/apollographql/federation/commit/b8e4ab5352a4dfd262af49493fdd42e86e5e3d99), [`e6c05b6c96023aa3dec79889431f8217fcb3806d`](https://github.com/apollographql/federation/commit/e6c05b6c96023aa3dec79889431f8217fcb3806d)]: - @apollo/federation-internals@2.9.1 - @apollo/query-graphs@2.9.1 ## @apollo/gateway@2.9.1 ### Patch Changes - Fix bugs in composition when merging nulls in directive applications and when handling renames. ([#3134](https://github.com/apollographql/federation/pull/3134)) - Updated dependencies \[[`b8e4ab5352a4dfd262af49493fdd42e86e5e3d99`](https://github.com/apollographql/federation/commit/b8e4ab5352a4dfd262af49493fdd42e86e5e3d99), [`e6c05b6c96023aa3dec79889431f8217fcb3806d`](https://github.com/apollographql/federation/commit/e6c05b6c96023aa3dec79889431f8217fcb3806d)]: - @apollo/federation-internals@2.9.1 - @apollo/composition@2.9.1 - @apollo/query-planner@2.9.1 ## @apollo/federation-internals@2.9.1 ### Patch Changes - Fix edge cases for subgraph extraction logic when using spec renaming or specs URLs that look similar to `specs.apollo.dev`. ([#3136](https://github.com/apollographql/federation/pull/3136)) - Fix bugs in composition when merging nulls in directive applications and when handling renames. ([#3134](https://github.com/apollographql/federation/pull/3134)) ## @apollo/query-graphs@2.9.1 ### Patch Changes - Updated dependencies \[[`b8e4ab5352a4dfd262af49493fdd42e86e5e3d99`](https://github.com/apollographql/federation/commit/b8e4ab5352a4dfd262af49493fdd42e86e5e3d99), [`e6c05b6c96023aa3dec79889431f8217fcb3806d`](https://github.com/apollographql/federation/commit/e6c05b6c96023aa3dec79889431f8217fcb3806d)]: - @apollo/federation-internals@2.9.1 ## @apollo/query-planner@2.9.1 ### Patch Changes - Updated dependencies \[[`b8e4ab5352a4dfd262af49493fdd42e86e5e3d99`](https://github.com/apollographql/federation/commit/b8e4ab5352a4dfd262af49493fdd42e86e5e3d99), [`e6c05b6c96023aa3dec79889431f8217fcb3806d`](https://github.com/apollographql/federation/commit/e6c05b6c96023aa3dec79889431f8217fcb3806d)]: - @apollo/federation-internals@2.9.1 - @apollo/query-graphs@2.9.1 ## @apollo/subgraph@2.9.1 ### Patch Changes - Updated dependencies \[[`b8e4ab5352a4dfd262af49493fdd42e86e5e3d99`](https://github.com/apollographql/federation/commit/b8e4ab5352a4dfd262af49493fdd42e86e5e3d99), [`e6c05b6c96023aa3dec79889431f8217fcb3806d`](https://github.com/apollographql/federation/commit/e6c05b6c96023aa3dec79889431f8217fcb3806d)]: - @apollo/federation-internals@2.9.1 ## apollo-federation-integration-testsuite@2.9.1 Co-authored-by: github-actions[bot] --- .changeset/light-ties-chew.md | 5 --- .changeset/proud-days-press.md | 7 ---- composition-js/CHANGELOG.md | 10 ++++++ composition-js/package.json | 6 ++-- .../CHANGELOG.md | 2 ++ .../package.json | 2 +- gateway-js/CHANGELOG.md | 11 +++++++ gateway-js/package.json | 8 ++--- internals-js/CHANGELOG.md | 8 +++++ internals-js/package.json | 2 +- package-lock.json | 32 +++++++++---------- query-graphs-js/CHANGELOG.md | 7 ++++ query-graphs-js/package.json | 4 +-- query-planner-js/CHANGELOG.md | 8 +++++ query-planner-js/package.json | 6 ++-- subgraph-js/CHANGELOG.md | 7 ++++ subgraph-js/package.json | 4 +-- 17 files changed, 85 insertions(+), 44 deletions(-) delete mode 100644 .changeset/light-ties-chew.md delete mode 100644 .changeset/proud-days-press.md diff --git a/.changeset/light-ties-chew.md b/.changeset/light-ties-chew.md deleted file mode 100644 index 805c9a403..000000000 --- a/.changeset/light-ties-chew.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/federation-internals": patch ---- - -Fix edge cases for subgraph extraction logic when using spec renaming or specs URLs that look similar to `specs.apollo.dev`. diff --git a/.changeset/proud-days-press.md b/.changeset/proud-days-press.md deleted file mode 100644 index 8330cbf9a..000000000 --- a/.changeset/proud-days-press.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@apollo/federation-internals": patch -"@apollo/gateway": patch -"@apollo/composition": patch ---- - -Fix bugs in composition when merging nulls in directive applications and when handling renames. diff --git a/composition-js/CHANGELOG.md b/composition-js/CHANGELOG.md index 12d6dbe2a..d613cff50 100644 --- a/composition-js/CHANGELOG.md +++ b/composition-js/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG for `@apollo/composition` +## 2.9.1 + +### Patch Changes + +- Fix bugs in composition when merging nulls in directive applications and when handling renames. ([#3134](https://github.com/apollographql/federation/pull/3134)) + +- Updated dependencies [[`b8e4ab5352a4dfd262af49493fdd42e86e5e3d99`](https://github.com/apollographql/federation/commit/b8e4ab5352a4dfd262af49493fdd42e86e5e3d99), [`e6c05b6c96023aa3dec79889431f8217fcb3806d`](https://github.com/apollographql/federation/commit/e6c05b6c96023aa3dec79889431f8217fcb3806d)]: + - @apollo/federation-internals@2.9.1 + - @apollo/query-graphs@2.9.1 + ## 2.9.0 ### Minor Changes diff --git a/composition-js/package.json b/composition-js/package.json index b6ac62614..81cae5f56 100644 --- a/composition-js/package.json +++ b/composition-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/composition", - "version": "2.9.0", + "version": "2.9.1", "description": "Apollo Federation composition utilities", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -27,8 +27,8 @@ "access": "public" }, "dependencies": { - "@apollo/federation-internals": "2.9.0", - "@apollo/query-graphs": "2.9.0" + "@apollo/federation-internals": "2.9.1", + "@apollo/query-graphs": "2.9.1" }, "peerDependencies": { "graphql": "^16.5.0" diff --git a/federation-integration-testsuite-js/CHANGELOG.md b/federation-integration-testsuite-js/CHANGELOG.md index bb20c7f64..9b9abbfcc 100644 --- a/federation-integration-testsuite-js/CHANGELOG.md +++ b/federation-integration-testsuite-js/CHANGELOG.md @@ -1,5 +1,7 @@ # CHANGELOG for `federation-integration-testsuite-js` +## 2.9.1 + ## 2.9.0 ## 2.8.5 diff --git a/federation-integration-testsuite-js/package.json b/federation-integration-testsuite-js/package.json index 8aa518ff6..78af2666b 100644 --- a/federation-integration-testsuite-js/package.json +++ b/federation-integration-testsuite-js/package.json @@ -1,7 +1,7 @@ { "name": "apollo-federation-integration-testsuite", "private": true, - "version": "2.9.0", + "version": "2.9.1", "description": "Apollo Federation Integrations / Test Fixtures", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md index e86f80054..40d44ff71 100644 --- a/gateway-js/CHANGELOG.md +++ b/gateway-js/CHANGELOG.md @@ -1,5 +1,16 @@ # CHANGELOG for `@apollo/gateway` +## 2.9.1 + +### Patch Changes + +- Fix bugs in composition when merging nulls in directive applications and when handling renames. ([#3134](https://github.com/apollographql/federation/pull/3134)) + +- Updated dependencies [[`b8e4ab5352a4dfd262af49493fdd42e86e5e3d99`](https://github.com/apollographql/federation/commit/b8e4ab5352a4dfd262af49493fdd42e86e5e3d99), [`e6c05b6c96023aa3dec79889431f8217fcb3806d`](https://github.com/apollographql/federation/commit/e6c05b6c96023aa3dec79889431f8217fcb3806d)]: + - @apollo/federation-internals@2.9.1 + - @apollo/composition@2.9.1 + - @apollo/query-planner@2.9.1 + ## 2.9.0 ### Patch Changes diff --git a/gateway-js/package.json b/gateway-js/package.json index 25605aa62..a039559da 100644 --- a/gateway-js/package.json +++ b/gateway-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/gateway", - "version": "2.9.0", + "version": "2.9.1", "description": "Apollo Gateway", "author": "Apollo ", "main": "dist/index.js", @@ -25,9 +25,9 @@ "access": "public" }, "dependencies": { - "@apollo/composition": "2.9.0", - "@apollo/federation-internals": "2.9.0", - "@apollo/query-planner": "2.9.0", + "@apollo/composition": "2.9.1", + "@apollo/federation-internals": "2.9.1", + "@apollo/query-planner": "2.9.1", "@apollo/server-gateway-interface": "^1.1.0", "@apollo/usage-reporting-protobuf": "^4.1.0", "@apollo/utils.createhash": "^2.0.0", diff --git a/internals-js/CHANGELOG.md b/internals-js/CHANGELOG.md index 1265c2550..d1f034240 100644 --- a/internals-js/CHANGELOG.md +++ b/internals-js/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG for `@apollo/federation-internals` +## 2.9.1 + +### Patch Changes + +- Fix edge cases for subgraph extraction logic when using spec renaming or specs URLs that look similar to `specs.apollo.dev`. ([#3136](https://github.com/apollographql/federation/pull/3136)) + +- Fix bugs in composition when merging nulls in directive applications and when handling renames. ([#3134](https://github.com/apollographql/federation/pull/3134)) + ## 2.9.0 ### Minor Changes diff --git a/internals-js/package.json b/internals-js/package.json index 6ab818e1d..b8b730103 100644 --- a/internals-js/package.json +++ b/internals-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/federation-internals", - "version": "2.9.0", + "version": "2.9.1", "description": "Apollo Federation internal utilities", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/package-lock.json b/package-lock.json index 730142f89..ca8578518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,11 +70,11 @@ }, "composition-js": { "name": "@apollo/composition", - "version": "2.9.0", + "version": "2.9.1", "license": "Elastic-2.0", "dependencies": { - "@apollo/federation-internals": "2.9.0", - "@apollo/query-graphs": "2.9.0" + "@apollo/federation-internals": "2.9.1", + "@apollo/query-graphs": "2.9.1" }, "engines": { "node": ">=14.15.0" @@ -85,7 +85,7 @@ }, "federation-integration-testsuite-js": { "name": "apollo-federation-integration-testsuite", - "version": "2.9.0", + "version": "2.9.1", "license": "Elastic-2.0", "dependencies": { "graphql-tag": "^2.12.6", @@ -94,12 +94,12 @@ }, "gateway-js": { "name": "@apollo/gateway", - "version": "2.9.0", + "version": "2.9.1", "license": "Elastic-2.0", "dependencies": { - "@apollo/composition": "2.9.0", - "@apollo/federation-internals": "2.9.0", - "@apollo/query-planner": "2.9.0", + "@apollo/composition": "2.9.1", + "@apollo/federation-internals": "2.9.1", + "@apollo/query-planner": "2.9.1", "@apollo/server-gateway-interface": "^1.1.0", "@apollo/usage-reporting-protobuf": "^4.1.0", "@apollo/utils.createhash": "^2.0.0", @@ -125,7 +125,7 @@ }, "internals-js": { "name": "@apollo/federation-internals", - "version": "2.9.0", + "version": "2.9.1", "license": "Elastic-2.0", "dependencies": { "@types/uuid": "^9.0.0", @@ -17859,10 +17859,10 @@ }, "query-graphs-js": { "name": "@apollo/query-graphs", - "version": "2.9.0", + "version": "2.9.1", "license": "Elastic-2.0", "dependencies": { - "@apollo/federation-internals": "2.9.0", + "@apollo/federation-internals": "2.9.1", "deep-equal": "^2.0.5", "ts-graphviz": "^1.5.4", "uuid": "^9.0.0" @@ -17876,11 +17876,11 @@ }, "query-planner-js": { "name": "@apollo/query-planner", - "version": "2.9.0", + "version": "2.9.1", "license": "Elastic-2.0", "dependencies": { - "@apollo/federation-internals": "2.9.0", - "@apollo/query-graphs": "2.9.0", + "@apollo/federation-internals": "2.9.1", + "@apollo/query-graphs": "2.9.1", "@apollo/utils.keyvaluecache": "^2.1.0", "chalk": "^4.1.0", "deep-equal": "^2.0.5", @@ -17909,11 +17909,11 @@ }, "subgraph-js": { "name": "@apollo/subgraph", - "version": "2.9.0", + "version": "2.9.1", "license": "MIT", "dependencies": { "@apollo/cache-control-types": "^1.0.2", - "@apollo/federation-internals": "2.9.0" + "@apollo/federation-internals": "2.9.1" }, "engines": { "node": ">=14.15.0" diff --git a/query-graphs-js/CHANGELOG.md b/query-graphs-js/CHANGELOG.md index 2b2b6686b..2dbd7fcc5 100644 --- a/query-graphs-js/CHANGELOG.md +++ b/query-graphs-js/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG for `@apollo/query-graphs` +## 2.9.1 + +### Patch Changes + +- Updated dependencies [[`b8e4ab5352a4dfd262af49493fdd42e86e5e3d99`](https://github.com/apollographql/federation/commit/b8e4ab5352a4dfd262af49493fdd42e86e5e3d99), [`e6c05b6c96023aa3dec79889431f8217fcb3806d`](https://github.com/apollographql/federation/commit/e6c05b6c96023aa3dec79889431f8217fcb3806d)]: + - @apollo/federation-internals@2.9.1 + ## 2.9.0 ### Patch Changes diff --git a/query-graphs-js/package.json b/query-graphs-js/package.json index 1760a4785..317caf322 100644 --- a/query-graphs-js/package.json +++ b/query-graphs-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/query-graphs", - "version": "2.9.0", + "version": "2.9.1", "description": "Apollo Federation library to work with 'query graphs'", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -23,7 +23,7 @@ "node": ">=14.15.0" }, "dependencies": { - "@apollo/federation-internals": "2.9.0", + "@apollo/federation-internals": "2.9.1", "deep-equal": "^2.0.5", "ts-graphviz": "^1.5.4", "uuid": "^9.0.0" diff --git a/query-planner-js/CHANGELOG.md b/query-planner-js/CHANGELOG.md index 35ccd0011..25e8f4153 100644 --- a/query-planner-js/CHANGELOG.md +++ b/query-planner-js/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG for `@apollo/query-planner` +## 2.9.1 + +### Patch Changes + +- Updated dependencies [[`b8e4ab5352a4dfd262af49493fdd42e86e5e3d99`](https://github.com/apollographql/federation/commit/b8e4ab5352a4dfd262af49493fdd42e86e5e3d99), [`e6c05b6c96023aa3dec79889431f8217fcb3806d`](https://github.com/apollographql/federation/commit/e6c05b6c96023aa3dec79889431f8217fcb3806d)]: + - @apollo/federation-internals@2.9.1 + - @apollo/query-graphs@2.9.1 + ## 2.9.0 ### Patch Changes diff --git a/query-planner-js/package.json b/query-planner-js/package.json index 73ae7b8a7..3a14f093e 100644 --- a/query-planner-js/package.json +++ b/query-planner-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/query-planner", - "version": "2.9.0", + "version": "2.9.1", "description": "Apollo Query Planner", "author": "Apollo ", "main": "dist/index.js", @@ -25,8 +25,8 @@ "access": "public" }, "dependencies": { - "@apollo/federation-internals": "2.9.0", - "@apollo/query-graphs": "2.9.0", + "@apollo/federation-internals": "2.9.1", + "@apollo/query-graphs": "2.9.1", "@apollo/utils.keyvaluecache": "^2.1.0", "chalk": "^4.1.0", "deep-equal": "^2.0.5", diff --git a/subgraph-js/CHANGELOG.md b/subgraph-js/CHANGELOG.md index 0abb1e797..1bbdf5a22 100644 --- a/subgraph-js/CHANGELOG.md +++ b/subgraph-js/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG for `@apollo/subgraph` +## 2.9.1 + +### Patch Changes + +- Updated dependencies [[`b8e4ab5352a4dfd262af49493fdd42e86e5e3d99`](https://github.com/apollographql/federation/commit/b8e4ab5352a4dfd262af49493fdd42e86e5e3d99), [`e6c05b6c96023aa3dec79889431f8217fcb3806d`](https://github.com/apollographql/federation/commit/e6c05b6c96023aa3dec79889431f8217fcb3806d)]: + - @apollo/federation-internals@2.9.1 + ## 2.9.0 ### Patch Changes diff --git a/subgraph-js/package.json b/subgraph-js/package.json index 3a1a03598..c718a90ff 100644 --- a/subgraph-js/package.json +++ b/subgraph-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/subgraph", - "version": "2.9.0", + "version": "2.9.1", "description": "Apollo Subgraph Utilities", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@apollo/cache-control-types": "^1.0.2", - "@apollo/federation-internals": "2.9.0" + "@apollo/federation-internals": "2.9.1" }, "peerDependencies": { "graphql": "^16.5.0" From 15fe9afd95e7e1c3e7b0e0fd4beb19f1f5eeff5d Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 20 Sep 2024 18:35:29 +0200 Subject: [PATCH 09/10] [docs]: Link fixes (#3149) Updates links --- docs/source/entities/migrate-fields.mdx | 2 +- .../federation-2/moving-to-federation-2.mdx | 12 ++++++------ .../federation-2/new-in-federation-2.mdx | 12 ++++++------ docs/source/federation-versions.mdx | 18 +++++++++--------- docs/source/hints.mdx | 2 +- docs/source/index.mdx | 2 +- docs/source/managed-federation/overview.mdx | 2 +- docs/source/subgraph-spec.mdx | 6 +++--- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/source/entities/migrate-fields.mdx b/docs/source/entities/migrate-fields.mdx index 106533eb7..f4668010a 100644 --- a/docs/source/entities/migrate-fields.mdx +++ b/docs/source/entities/migrate-fields.mdx @@ -322,7 +322,7 @@ For most use cases, Apollo recommends using the [`@override` method above](#incr Using `@override` to migrate entity fields enables us to migrate fields incrementally with zero downtime. However, doing so requires three separate schema publishes. -If you're using [manual composition](./federated-types/composition#manually-with-the-rover-cli), each schema change requires redeploying your router. +If you're using [manual composition](./federated-schemas/composition#manually-with-the-rover-cli), each schema change requires redeploying your router. With careful coordination, you can perform the same migration with only a single router redeploy. 1. In the Billing subgraph, define the `Bill` entity, along with its corresponding resolvers. These new resolvers should behave identically to the Payment subgraph resolvers they're replacing. diff --git a/docs/source/federation-2/moving-to-federation-2.mdx b/docs/source/federation-2/moving-to-federation-2.mdx index e02e1efd5..4dcf057da 100644 --- a/docs/source/federation-2/moving-to-federation-2.mdx +++ b/docs/source/federation-2/moving-to-federation-2.mdx @@ -168,9 +168,9 @@ You can update your subgraphs one at a time. The steps below describe how to mod Federation 2 provides powerful new features that require making some changes to your subgraphs. These features include: -- Selectively sharing types and fields across subgraphs with [`@shareable`](../federated-types/federated-directives#shareable) -- Safely migrating fields from one subgraph to another with [`@override`](../federated-types/federated-directives#override) -- Hiding internal routing fields from graph consumers with [`@inaccessible`](../federated-types/federated-directives#inaccessible) +- Selectively sharing types and fields across subgraphs with [`@shareable`](../federated-schemas/federated-directives#shareable) +- Safely migrating fields from one subgraph to another with [`@override`](../federated-schemas/federated-directives#override) +- Hiding internal routing fields from graph consumers with [`@inaccessible`](../federated-schemas/federated-directives#inaccessible) The schema changes you make are not backward compatible with Federation 1, which means you won't be able to use Federation 1 composition anymore unless you revert those changes. @@ -203,7 +203,7 @@ This definition identifies a schema as a Federation 2 schema, and it `import`s a Depending on your schema, you might need to add other federated directives to the `import` array, such as `@external` or `@provides`. -[See all Federation-specific directives.](../federated-types/federated-directives/) +[See all Federation-specific directives.](../federated-schemas/federated-directives/) @@ -234,7 +234,7 @@ Some subgraph libraries are "code-first" (they dynamically generate their schema -Definitions for all Federation 2 directives are available in [this article](../federated-types/federated-directives/). We work with library maintainers to help automatically add these schema definitions in as many subgraph libraries as possible. +Definitions for all Federation 2 directives are available in [this article](../federated-schemas/federated-directives/). We work with library maintainers to help automatically add these schema definitions in as many subgraph libraries as possible. ### Mark all value types as `@shareable` @@ -288,7 +288,7 @@ You can also apply `@shareable` directly to a type definition (such as `Position -For more details, see [Value types](../federated-types/sharing-types/). +For more details, see [Value types](../federated-schemas/sharing-types/). ### Update entity definitions diff --git a/docs/source/federation-2/new-in-federation-2.mdx b/docs/source/federation-2/new-in-federation-2.mdx index 119535543..be90b3578 100644 --- a/docs/source/federation-2/new-in-federation-2.mdx +++ b/docs/source/federation-2/new-in-federation-2.mdx @@ -55,7 +55,7 @@ type Book { In Federation 2, this "identical definition" constraint is removed. Value types and their fields can be shared across subgraphs, even if certain details differ between definitions. -For details, see the sections below, along with [Value types](../federated-types/sharing-types/). +For details, see the sections below, along with [Value types](../federated-schemas/sharing-types/). ### Objects @@ -82,7 +82,7 @@ type Book @shareable { -The two `Book` type definitions above differ in terms of the fields they include and the nullability of those fields. Notice also the new [`@shareable`](../federated-types/sharing-types/#using-shareable) directive, which is required to indicate that a field can be resolved by multiple subgraphs. +The two `Book` type definitions above differ in terms of the fields they include and the nullability of those fields. Notice also the new [`@shareable`](../federated-schemas/sharing-types/#using-shareable) directive, which is required to indicate that a field can be resolved by multiple subgraphs. @@ -95,13 +95,13 @@ This flexibility is especially helpful when an organization has multiple standal #### Valid shared field differences between subgraphs * The return type of a shared field can vary in nullability (`String` / `String!`). -* Types can omit fields that are included in other subgraphs, as long as every field in your supergraph is always resolvable. (For details, see [Rules of composition](../federated-types/composition/#rules-of-composition).) +* Types can omit fields that are included in other subgraphs, as long as every field in your supergraph is always resolvable. (For details, see [Rules of composition](../federated-schemas/composition/#rules-of-composition).) -For details on how these field differences are handled, see [Differing shared fields](../federated-types/sharing-types/#differing-shared-fields). +For details on how these field differences are handled, see [Differing shared fields](../federated-schemas/sharing-types/#differing-shared-fields). ### Enums, unions, and interfaces -In Federation 2, `enum`, `union`, and `interface` definitions can differ between subgraphs. For details, see [Merging types from multiple subgraphs](../federated-types/composition/#merging-types-from-multiple-subgraphs). +In Federation 2, `enum`, `union`, and `interface` definitions can differ between subgraphs. For details, see [Merging types from multiple subgraphs](../federated-schemas/composition/#merging-types-from-multiple-subgraphs). ## Entities @@ -222,7 +222,7 @@ For more information, see [Referencing an entity without contributing fields](.. ### Migrating fields -Federation 2 introduces the [`@override` directive](../federated-types/federated-directives/#override), which helps you safely migrate entity and root-level fields between subgraphs with managed federation: +Federation 2 introduces the [`@override` directive](../federated-schemas/federated-directives/#override), which helps you safely migrate entity and root-level fields between subgraphs with managed federation: diff --git a/docs/source/federation-versions.mdx b/docs/source/federation-versions.mdx index b1cebbb1f..d2143ec0b 100644 --- a/docs/source/federation-versions.mdx +++ b/docs/source/federation-versions.mdx @@ -4,7 +4,7 @@ subtitle: Understand changes between Apollo Federation versions description: Understand changes between Apollo Federation major and minor versions. --- -This article describes notable changes and additions introduced in each minor version release of Apollo Federation. Most of these changes involve additions or modifications to [federation-specific directives](./federated-types/federated-directives/). +This article describes notable changes and additions introduced in each minor version release of Apollo Federation. Most of these changes involve additions or modifications to [federation-specific directives](./federated-schemas/federated-directives/). For a comprehensive changelog for Apollo Federation and its associated libraries, see [GitHub](https://github.com/apollographql/federation/blob/main/CHANGELOG.md). @@ -71,7 +71,7 @@ Minimum router version -Introduced. [Learn more](./federated-types/federated-directives/#cost). +Introduced. [Learn more](./federated-schemas/federated-directives/#cost). ```graphql directive @cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR @@ -88,7 +88,7 @@ directive @cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | -Introduced. [Learn more](./federated-types/federated-directives/#listsize). +Introduced. [Learn more](./federated-schemas/federated-directives/#listsize). ```graphql directive @listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION @@ -153,7 +153,7 @@ Minimum router version -Introduced. [Learn more](./federated-types/federated-directives/#context). +Introduced. [Learn more](./federated-schemas/federated-directives/#context). ```graphql directive @context(name: String!) on OBJECT | INTERFACE | UNION; @@ -170,7 +170,7 @@ directive @context(name: String!) on OBJECT | INTERFACE | UNION; -Introduced. [Learn more](./federated-types/federated-directives/#fromcontext). +Introduced. [Learn more](./federated-schemas/federated-directives/#fromcontext). ```graphql scalar ContextFieldValue; @@ -237,7 +237,7 @@ Minimum router version -Added progressive `@override`. [Learn more.](./federated-types/federated-directives/#progressive-override) +Added progressive `@override`. [Learn more.](./federated-schemas/federated-directives/#progressive-override) ```graphql directive @override(from: String!, label: String) on FIELD_DEFINITION @@ -728,7 +728,7 @@ Minimum router version -Introduced. [Learn more.](./federated-types/federated-directives#composedirective) +Introduced. [Learn more.](./federated-schemas/federated-directives#composedirective) ```graphql directive @composeDirective(name: String!) repeatable on SCHEMA @@ -811,7 +811,7 @@ extend schema The `import` list of this definition must include each federation-specific directive that the subgraph schema uses. In the example above, the schema uses `@key` and `@shareable`. -For details on these directives as defined in Federation 2, see [Federation-specific GraphQL directives](./federated-types/federated-directives/). +For details on these directives as defined in Federation 2, see [Federation-specific GraphQL directives](./federated-schemas/federated-directives/). @@ -978,7 +978,7 @@ Value types diff --git a/docs/source/hints.mdx b/docs/source/hints.mdx index c55371ea6..9034cee1a 100644 --- a/docs/source/hints.mdx +++ b/docs/source/hints.mdx @@ -4,7 +4,7 @@ subtitle: Reference for composition hints description: Learn about hints flagged during Apollo Federation schema composition using GraphOS Studio or the Rover CLI. --- -When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process can flag potential improvements or hints. Hints are violations of the [GraphOS schema linter's](/graphos/delivery/schema-linter) [composition rules](/graphos/delivery/linter-rules#composition-rules). You can review them on the [Checks](/graphos/delivery/schema-checks) page in GraphOS Studio or via the [Rover CLI](/rover/). +When you successfully [compose](./federated-schemas/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process can flag potential improvements or hints. Hints are violations of the [GraphOS schema linter's](/graphos/delivery/schema-linter) [composition rules](/graphos/delivery/linter-rules#composition-rules). You can review them on the [Checks](/graphos/delivery/schema-checks) page in GraphOS Studio or via the [Rover CLI](/rover/). diff --git a/docs/source/index.mdx b/docs/source/index.mdx index 5e3337a69..4d66cb665 100644 --- a/docs/source/index.mdx +++ b/docs/source/index.mdx @@ -162,7 +162,7 @@ Depending on your goals, you have several options for learning more about federa Once you're ready to apply federation to your own APIs, these docs sections can help you get started: - [Quickstart](./quickstart) to get you up and running with a federated graph -- Conceptual overview of [Federated Schemas](./federated-types/overview) +- Conceptual overview of [Federated Schemas](./federated-schemas) - Reference materials for: - [Performance considerations](./performance/caching) - [Debugging and metrics](./errors) diff --git a/docs/source/managed-federation/overview.mdx b/docs/source/managed-federation/overview.mdx index 96fabad1f..1d4a4df96 100644 --- a/docs/source/managed-federation/overview.mdx +++ b/docs/source/managed-federation/overview.mdx @@ -10,7 +10,7 @@ import ManagedFederationDiagram from '../../shared/diagrams/managed-federation.m With managed federation, you maintain subgraphs and delegate GraphOS to manage CI/CD tasks including the validation, composition, and update of your supergraph: -* Your subgraphs publish their schemas to GraphOS, which stores them in its schema registry. GraphOS then [validates](./federated-schema-checks) and [composes](../federated-types/composition/) them into a supergraph schema. +* Your subgraphs publish their schemas to GraphOS, which stores them in its schema registry. GraphOS then [validates](./federated-schema-checks) and [composes](../federated-schemas/composition/) them into a supergraph schema. * Your routers can poll GraphOS—specifically, its [Apollo Uplink](./uplink) endpoint—to get the latest validated supergraph schema and other configurations. diff --git a/docs/source/subgraph-spec.mdx b/docs/source/subgraph-spec.mdx index 42f59b2a5..7eb9d49e3 100644 --- a/docs/source/subgraph-spec.mdx +++ b/docs/source/subgraph-spec.mdx @@ -105,7 +105,7 @@ Apollo strongly recommends against dynamic composition in the graph router. Dyna The "enhanced" introspection query above differs from [the GraphQL spec's built-in introspection query](https://spec.graphql.org/October2021/#sec-Schema-Introspection) in the following ways: - The returned schema representation is a string instead of a `__Schema` object. -- The returned schema string includes all uses of [federation-specific directives](./federated-types/federated-directives/), such as `@key`. +- The returned schema string includes all uses of [federation-specific directives](./federated-schemas/federated-directives/), such as `@key`. - The built-in introspection query's response does not include the uses of any directives. - The graph router requires these federation-specific directives to perform composition successfully. - If a subgraph server "disables introspection", the enhanced introspection query is still available. @@ -412,7 +412,7 @@ Your subgraph library does not need to use this reference resolver pattern. It j This section describes type and field definitions that a valid subgraph service must automatically add to its schema. These definitions are all listed above in [Subgraph schema additions](#subgraph-schema-additions). -For descriptions of added directives, see [Federation-specific GraphQL directives](./federated-types/federated-directives/). +For descriptions of added directives, see [Federation-specific GraphQL directives](./federated-schemas/federated-directives/). ### `Query` fields @@ -488,4 +488,4 @@ This string-serialized scalar represents an authorization policy. ### Directives -See [Federation-specific GraphQL directives](./federated-types/federated-directives/). +See [Federation-specific GraphQL directives](./federated-schemas/federated-directives/). From 4b3bcbbb47362b3586e0e8ec5de05ce46b4696b5 Mon Sep 17 00:00:00 2001 From: Juan Gonzalez Date: Fri, 20 Sep 2024 16:47:21 -0400 Subject: [PATCH 10/10] Fix broken link to Federation Quickstart (#3150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixing a broken link to Federation Quickstart. Broken link appears [here in the docs](https://www.apollographql.com/docs/federation/building-supergraphs/router/#choosing-a-router-library) --- docs/source/building-supergraphs/router.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/building-supergraphs/router.mdx b/docs/source/building-supergraphs/router.mdx index 56b9b3e13..28cca4634 100644 --- a/docs/source/building-supergraphs/router.mdx +++ b/docs/source/building-supergraphs/router.mdx @@ -26,7 +26,7 @@ Apollo actively supports the following options for your router: - **The graphos Router (recommended)**: This is a high-performance, precompiled Rust binary. - If you're getting started with federation, we recommend [creating a cloud supergraph](/graphos/quickstart/cloud/) with Apollo GraphOS. With a cloud supergraph, GraphOS provisions and manages your router for you. - - You can also host your own GraphOS Router instances. [See the Federation Quickstart](../quickstart/setup/) to get started. + - You can also host your own GraphOS Router instances. [See the Federation Quickstart](../quickstart/]) to get started. - **Apollo Server**: Apollo Server can act as your router via the [`@apollo/gateway`](https://www.npmjs.com/package/@apollo/gateway) extension library. - [See how to set up Apollo Gateway](/apollo-server/using-federation/apollo-gateway-setup).
* To define a value type with shared fields across multiple subgraphs, those shared fields must be marked as `@shareable` in every subgraph that defines them. -* Value type fields can differ across subgraphs (in certain ways). For details, see [Differing shared fields](./federated-types/sharing-types#differing-shared-fields). +* Value type fields can differ across subgraphs (in certain ways). For details, see [Differing shared fields](./federated-schemas/sharing-types#differing-shared-fields).