Skip to content

Commit

Permalink
Merge branch 'main' into trevor/progressive-override-hints
Browse files Browse the repository at this point in the history
  • Loading branch information
trevor-scheer authored Feb 5, 2024
2 parents bee4a8b + 33b937b commit 151ddd7
Show file tree
Hide file tree
Showing 13 changed files with 638 additions and 90 deletions.
6 changes: 6 additions & 0 deletions .changeset/forty-dolls-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@apollo/composition": minor
"@apollo/federation-internals": minor
---

When a linked directive requires a federation version higher than the linked federation spec, upgrade to the implied version and issue a hint
4 changes: 0 additions & 4 deletions composition-js/src/__tests__/compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5101,10 +5101,6 @@ describe('@source* directives', () => {

const messages = result.errors!.map(e => e.message);

expect(messages).toContain(
'[bad] Schemas that @link to https://specs.apollo.dev/source must also @link to federation version v2.7 or later (found v2.5)'
);

expect(messages).toContain(
'[bad] @sourceAPI(name: "A?!") must specify name using only [a-zA-Z0-9-_] characters'
);
Expand Down
121 changes: 120 additions & 1 deletion composition-js/src/__tests__/hints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
HINTS,
} from '../hints';
import { MergeResult, mergeSubgraphs } from '../merging';
import { composeAsFed2Subgraphs } from './testHelper';
import { assertCompositionSuccess, composeAsFed2Subgraphs } from './testHelper';
import { formatExpectedToMatchReceived } from './matchers/toMatchString';
import { composeServices } from '../compose';

function mergeDocuments(...documents: DocumentNode[]): MergeResult {
const subgraphs = new Subgraphs();
Expand Down Expand Up @@ -1235,3 +1236,121 @@ describe('when shared field has intersecting but non equal runtime types in diff
);
});
});

describe('when a directive causes an implicit federation version upgrade', () => {
const olderFederationSchema = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])
type Query {
a: String!
}
`;

const newerFederationSchema = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"])
type Query {
b: String!
}
`;

const autoUpgradedSchema = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@shareable"])
@link(url: "https://specs.apollo.dev/source/v0.1", import: [
"@sourceAPI"
"@sourceType"
"@sourceField"
])
@sourceAPI(
name: "A"
http: { baseURL: "https://api.a.com/v1" }
)
{
query: Query
}
type Query @shareable {
resources: [Resource!]! @sourceField(
api: "A"
http: { GET: "/resources" }
)
}
type Resource @shareable @key(fields: "id") @sourceType(
api: "A"
http: { GET: "/resources/{id}" }
selection: "id description"
) {
id: ID!
description: String!
}
`;

it('should hint that the version was upgraded to satisfy directive requirements', () => {
const result = composeServices([
{
name: 'already-newest',
typeDefs: newerFederationSchema,
},
{
name: 'old-but-not-upgraded',
typeDefs: olderFederationSchema,
},
{
name: 'upgraded',
typeDefs: autoUpgradedSchema,
}
]);

assertCompositionSuccess(result);
expect(result).toRaiseHint(
HINTS.IMPLICITLY_UPGRADED_FEDERATION_VERSION,
'Subgraph upgraded has been implicitly upgraded from federation v2.5 to v2.7',
'@link'
);
});

it('should show separate hints for each upgraded subgraph', () => {
const result = composeServices([
{
name: 'upgraded-1',
typeDefs: autoUpgradedSchema,
},
{
name: 'upgraded-2',
typeDefs: autoUpgradedSchema
},
]);

assertCompositionSuccess(result);
expect(result).toRaiseHint(
HINTS.IMPLICITLY_UPGRADED_FEDERATION_VERSION,
'Subgraph upgraded-1 has been implicitly upgraded from federation v2.5 to v2.7',
'@link'
);
expect(result).toRaiseHint(
HINTS.IMPLICITLY_UPGRADED_FEDERATION_VERSION,
'Subgraph upgraded-2 has been implicitly upgraded from federation v2.5 to v2.7',
'@link'
);
});

it('should not raise hints if the only upgrade is caused by a link directly to the federation spec', () => {
const result = composeServices([
{
name: 'already-newest',
typeDefs: newerFederationSchema,
},
{
name: 'old-but-not-upgraded',
typeDefs: olderFederationSchema,
},
]);

assertCompositionSuccess(result);
expect(result).toNotRaiseHints();
});
})
8 changes: 8 additions & 0 deletions composition-js/src/hints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ const INCONSISTENT_RUNTIME_TYPES_FOR_SHAREABLE_RETURN = makeCodeDefinition({
description: 'Indicates that a @shareable field returns different sets of runtime types in the different subgraphs in which it is defined.',
});

const IMPLICITLY_UPGRADED_FEDERATION_VERSION = makeCodeDefinition({
code: 'IMPLICITLY_UPGRADED_FEDERATION_VERSION',
level: HintLevel.INFO,
description: 'Indicates that a directive requires a higher federation version than is explicitly linked.'
+ ' In this case, the supergraph uses the federation version required by the directive.'
});

export const HINTS = {
INCONSISTENT_BUT_COMPATIBLE_FIELD_TYPE,
INCONSISTENT_BUT_COMPATIBLE_ARGUMENT_TYPE,
Expand Down Expand Up @@ -234,6 +241,7 @@ export const HINTS = {
DIRECTIVE_COMPOSITION_INFO,
DIRECTIVE_COMPOSITION_WARN,
INCONSISTENT_RUNTIME_TYPES_FOR_SHAREABLE_RETURN,
IMPLICITLY_UPGRADED_FEDERATION_VERSION,
}

export class CompositionHint {
Expand Down
57 changes: 48 additions & 9 deletions composition-js/src/merging/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ import {
LinkDirectiveArgs,
sourceIdentity,
FeatureUrl,
CoreFeature,
Subgraph,
} from "@apollo/federation-internals";
import { ASTNode, GraphQLError, DirectiveLocation } from "graphql";
import {
Expand Down Expand Up @@ -343,19 +345,56 @@ class Merger {
}

private getLatestFederationVersionUsed(): FeatureVersion {
const latestVersion = this.subgraphs.values().reduce((latest: FeatureVersion | undefined, subgraph) => {
const version = subgraph.metadata()?.federationFeature()?.url?.version;
if (!latest) {
return version;
const versions = this.subgraphs.values()
.map((s) => this.getLatestFederationVersionUsedInSubgraph(s))
.filter(isDefined);

return FeatureVersion.max(versions) ?? FEDERATION_VERSIONS.latest().version;
}

private getLatestFederationVersionUsedInSubgraph(subgraph: Subgraph): FeatureVersion | undefined {
const linkedFederationVersion = subgraph.metadata()?.federationFeature()?.url.version;
if (!linkedFederationVersion) {
return undefined;
}

// Check if any of the directives imply a newer version of federation than is explicitly linked
const versionsFromFeatures: FeatureVersion[] = [];
for (const feature of subgraph.schema.coreFeatures?.allFeatures() ?? []) {
const version = feature.minimumFederationVersion();
if (version) {
versionsFromFeatures.push(version);
}
if (!version) {
return latest;
}
const impliedFederationVersion = FeatureVersion.max(versionsFromFeatures);
if (!impliedFederationVersion?.satisfies(linkedFederationVersion) || linkedFederationVersion >= impliedFederationVersion) {
return linkedFederationVersion;
}

// If some of the directives are causing an implicit upgrade, put one in the hint
let featureCausingUpgrade: CoreFeature | undefined;
for (const feature of subgraph.schema.coreFeatures?.allFeatures() ?? []) {
if (feature.minimumFederationVersion() == impliedFederationVersion) {
featureCausingUpgrade = feature;
break;
}
return latest >= version ? latest : version;
}, undefined);
return latestVersion ?? FEDERATION_VERSIONS.latest().version;
}

if (featureCausingUpgrade) {
this.hints.push(new CompositionHint(
HINTS.IMPLICITLY_UPGRADED_FEDERATION_VERSION,
`Subgraph ${subgraph.name} has been implicitly upgraded from federation ${linkedFederationVersion} to ${impliedFederationVersion}`,
featureCausingUpgrade.directive.definition,
featureCausingUpgrade.directive.sourceAST ?
addSubgraphToASTNode(featureCausingUpgrade.directive.sourceAST, subgraph.name) :
undefined
));
}

return impliedFederationVersion;
}


private prepareSupergraph(): Map<string, string> {
// TODO: we will soon need to look for name conflicts for @core and @join with potentially user-defined directives and
// pass a `as` to the methods below if necessary. However, as we currently don't propagate any subgraph directives to
Expand Down
5 changes: 5 additions & 0 deletions docs/shared/enterprise-directive.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<EnterpriseFeature>

This directive is an [Enterprise feature](/router/enterprise-features) of the Apollo Router and requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/). If your organization doesn't have an Enterprise plan, you can test it out by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials).

</EnterpriseFeature>
5 changes: 5 additions & 0 deletions docs/shared/progressive-override-enterprise.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<EnterpriseFeature>

Progressive `@override` is an [Enterprise feature](/router/enterprise-features) of the Apollo Router and requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/). If your organization doesn't have an Enterprise plan, you can test it out by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials).

</EnterpriseFeature>
Loading

0 comments on commit 151ddd7

Please sign in to comment.