Skip to content

Commit 151ddd7

Browse files
Merge branch 'main' into trevor/progressive-override-hints
2 parents bee4a8b + 33b937b commit 151ddd7

File tree

13 files changed

+638
-90
lines changed

13 files changed

+638
-90
lines changed

.changeset/forty-dolls-type.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@apollo/composition": minor
3+
"@apollo/federation-internals": minor
4+
---
5+
6+
When a linked directive requires a federation version higher than the linked federation spec, upgrade to the implied version and issue a hint

composition-js/src/__tests__/compose.test.ts

-4
Original file line numberDiff line numberDiff line change
@@ -5101,10 +5101,6 @@ describe('@source* directives', () => {
51015101

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

5104-
expect(messages).toContain(
5105-
'[bad] Schemas that @link to https://specs.apollo.dev/source must also @link to federation version v2.7 or later (found v2.5)'
5106-
);
5107-
51085104
expect(messages).toContain(
51095105
'[bad] @sourceAPI(name: "A?!") must specify name using only [a-zA-Z0-9-_] characters'
51105106
);

composition-js/src/__tests__/hints.test.ts

+120-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {
66
HINTS,
77
} from '../hints';
88
import { MergeResult, mergeSubgraphs } from '../merging';
9-
import { composeAsFed2Subgraphs } from './testHelper';
9+
import { assertCompositionSuccess, composeAsFed2Subgraphs } from './testHelper';
1010
import { formatExpectedToMatchReceived } from './matchers/toMatchString';
11+
import { composeServices } from '../compose';
1112

1213
function mergeDocuments(...documents: DocumentNode[]): MergeResult {
1314
const subgraphs = new Subgraphs();
@@ -1235,3 +1236,121 @@ describe('when shared field has intersecting but non equal runtime types in diff
12351236
);
12361237
});
12371238
});
1239+
1240+
describe('when a directive causes an implicit federation version upgrade', () => {
1241+
const olderFederationSchema = gql`
1242+
extend schema
1243+
@link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])
1244+
1245+
type Query {
1246+
a: String!
1247+
}
1248+
`;
1249+
1250+
const newerFederationSchema = gql`
1251+
extend schema
1252+
@link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"])
1253+
1254+
type Query {
1255+
b: String!
1256+
}
1257+
`;
1258+
1259+
const autoUpgradedSchema = gql`
1260+
extend schema
1261+
@link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@shareable"])
1262+
@link(url: "https://specs.apollo.dev/source/v0.1", import: [
1263+
"@sourceAPI"
1264+
"@sourceType"
1265+
"@sourceField"
1266+
])
1267+
@sourceAPI(
1268+
name: "A"
1269+
http: { baseURL: "https://api.a.com/v1" }
1270+
)
1271+
{
1272+
query: Query
1273+
}
1274+
1275+
type Query @shareable {
1276+
resources: [Resource!]! @sourceField(
1277+
api: "A"
1278+
http: { GET: "/resources" }
1279+
)
1280+
}
1281+
1282+
type Resource @shareable @key(fields: "id") @sourceType(
1283+
api: "A"
1284+
http: { GET: "/resources/{id}" }
1285+
selection: "id description"
1286+
) {
1287+
id: ID!
1288+
description: String!
1289+
}
1290+
`;
1291+
1292+
it('should hint that the version was upgraded to satisfy directive requirements', () => {
1293+
const result = composeServices([
1294+
{
1295+
name: 'already-newest',
1296+
typeDefs: newerFederationSchema,
1297+
},
1298+
{
1299+
name: 'old-but-not-upgraded',
1300+
typeDefs: olderFederationSchema,
1301+
},
1302+
{
1303+
name: 'upgraded',
1304+
typeDefs: autoUpgradedSchema,
1305+
}
1306+
]);
1307+
1308+
assertCompositionSuccess(result);
1309+
expect(result).toRaiseHint(
1310+
HINTS.IMPLICITLY_UPGRADED_FEDERATION_VERSION,
1311+
'Subgraph upgraded has been implicitly upgraded from federation v2.5 to v2.7',
1312+
'@link'
1313+
);
1314+
});
1315+
1316+
it('should show separate hints for each upgraded subgraph', () => {
1317+
const result = composeServices([
1318+
{
1319+
name: 'upgraded-1',
1320+
typeDefs: autoUpgradedSchema,
1321+
},
1322+
{
1323+
name: 'upgraded-2',
1324+
typeDefs: autoUpgradedSchema
1325+
},
1326+
]);
1327+
1328+
assertCompositionSuccess(result);
1329+
expect(result).toRaiseHint(
1330+
HINTS.IMPLICITLY_UPGRADED_FEDERATION_VERSION,
1331+
'Subgraph upgraded-1 has been implicitly upgraded from federation v2.5 to v2.7',
1332+
'@link'
1333+
);
1334+
expect(result).toRaiseHint(
1335+
HINTS.IMPLICITLY_UPGRADED_FEDERATION_VERSION,
1336+
'Subgraph upgraded-2 has been implicitly upgraded from federation v2.5 to v2.7',
1337+
'@link'
1338+
);
1339+
});
1340+
1341+
it('should not raise hints if the only upgrade is caused by a link directly to the federation spec', () => {
1342+
const result = composeServices([
1343+
{
1344+
name: 'already-newest',
1345+
typeDefs: newerFederationSchema,
1346+
},
1347+
{
1348+
name: 'old-but-not-upgraded',
1349+
typeDefs: olderFederationSchema,
1350+
},
1351+
]);
1352+
1353+
assertCompositionSuccess(result);
1354+
expect(result).toNotRaiseHints();
1355+
});
1356+
})

composition-js/src/hints.ts

+8
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,13 @@ const INCONSISTENT_RUNTIME_TYPES_FOR_SHAREABLE_RETURN = makeCodeDefinition({
205205
description: 'Indicates that a @shareable field returns different sets of runtime types in the different subgraphs in which it is defined.',
206206
});
207207

208+
const IMPLICITLY_UPGRADED_FEDERATION_VERSION = makeCodeDefinition({
209+
code: 'IMPLICITLY_UPGRADED_FEDERATION_VERSION',
210+
level: HintLevel.INFO,
211+
description: 'Indicates that a directive requires a higher federation version than is explicitly linked.'
212+
+ ' In this case, the supergraph uses the federation version required by the directive.'
213+
});
214+
208215
export const HINTS = {
209216
INCONSISTENT_BUT_COMPATIBLE_FIELD_TYPE,
210217
INCONSISTENT_BUT_COMPATIBLE_ARGUMENT_TYPE,
@@ -234,6 +241,7 @@ export const HINTS = {
234241
DIRECTIVE_COMPOSITION_INFO,
235242
DIRECTIVE_COMPOSITION_WARN,
236243
INCONSISTENT_RUNTIME_TYPES_FOR_SHAREABLE_RETURN,
244+
IMPLICITLY_UPGRADED_FEDERATION_VERSION,
237245
}
238246

239247
export class CompositionHint {

composition-js/src/merging/merge.ts

+48-9
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ import {
7474
LinkDirectiveArgs,
7575
sourceIdentity,
7676
FeatureUrl,
77+
CoreFeature,
78+
Subgraph,
7779
} from "@apollo/federation-internals";
7880
import { ASTNode, GraphQLError, DirectiveLocation } from "graphql";
7981
import {
@@ -343,19 +345,56 @@ class Merger {
343345
}
344346

345347
private getLatestFederationVersionUsed(): FeatureVersion {
346-
const latestVersion = this.subgraphs.values().reduce((latest: FeatureVersion | undefined, subgraph) => {
347-
const version = subgraph.metadata()?.federationFeature()?.url?.version;
348-
if (!latest) {
349-
return version;
348+
const versions = this.subgraphs.values()
349+
.map((s) => this.getLatestFederationVersionUsedInSubgraph(s))
350+
.filter(isDefined);
351+
352+
return FeatureVersion.max(versions) ?? FEDERATION_VERSIONS.latest().version;
353+
}
354+
355+
private getLatestFederationVersionUsedInSubgraph(subgraph: Subgraph): FeatureVersion | undefined {
356+
const linkedFederationVersion = subgraph.metadata()?.federationFeature()?.url.version;
357+
if (!linkedFederationVersion) {
358+
return undefined;
359+
}
360+
361+
// Check if any of the directives imply a newer version of federation than is explicitly linked
362+
const versionsFromFeatures: FeatureVersion[] = [];
363+
for (const feature of subgraph.schema.coreFeatures?.allFeatures() ?? []) {
364+
const version = feature.minimumFederationVersion();
365+
if (version) {
366+
versionsFromFeatures.push(version);
350367
}
351-
if (!version) {
352-
return latest;
368+
}
369+
const impliedFederationVersion = FeatureVersion.max(versionsFromFeatures);
370+
if (!impliedFederationVersion?.satisfies(linkedFederationVersion) || linkedFederationVersion >= impliedFederationVersion) {
371+
return linkedFederationVersion;
372+
}
373+
374+
// If some of the directives are causing an implicit upgrade, put one in the hint
375+
let featureCausingUpgrade: CoreFeature | undefined;
376+
for (const feature of subgraph.schema.coreFeatures?.allFeatures() ?? []) {
377+
if (feature.minimumFederationVersion() == impliedFederationVersion) {
378+
featureCausingUpgrade = feature;
379+
break;
353380
}
354-
return latest >= version ? latest : version;
355-
}, undefined);
356-
return latestVersion ?? FEDERATION_VERSIONS.latest().version;
381+
}
382+
383+
if (featureCausingUpgrade) {
384+
this.hints.push(new CompositionHint(
385+
HINTS.IMPLICITLY_UPGRADED_FEDERATION_VERSION,
386+
`Subgraph ${subgraph.name} has been implicitly upgraded from federation ${linkedFederationVersion} to ${impliedFederationVersion}`,
387+
featureCausingUpgrade.directive.definition,
388+
featureCausingUpgrade.directive.sourceAST ?
389+
addSubgraphToASTNode(featureCausingUpgrade.directive.sourceAST, subgraph.name) :
390+
undefined
391+
));
392+
}
393+
394+
return impliedFederationVersion;
357395
}
358396

397+
359398
private prepareSupergraph(): Map<string, string> {
360399
// TODO: we will soon need to look for name conflicts for @core and @join with potentially user-defined directives and
361400
// pass a `as` to the methods below if necessary. However, as we currently don't propagate any subgraph directives to

docs/shared/enterprise-directive.mdx

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<EnterpriseFeature>
2+
3+
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).
4+
5+
</EnterpriseFeature>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<EnterpriseFeature>
2+
3+
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).
4+
5+
</EnterpriseFeature>

0 commit comments

Comments
 (0)