Skip to content

Commit 74fddf9

Browse files
feat(federation): Use extensions instead of custom federation objects (apollographql/apollo-server#4313)
Note: BREAKING CHANGE In order to guarantee the safe usage of extensions, we must now require `graphql@>14.5.0`. As such, we've narrowed the range in federation and gateway's `peerDependencies`. Composition in its current form modifies the graphql-js types in order to accommodate some additional metadata required during composition, validation, query planning, and execution. This is no longer required due to the addition of 'extensions', which this is intended to be used exactly for. This commit transitions our usages of these custom metadata objects into the extensions object where they belong. Apollo-Orig-Commit-AS: apollographql/apollo-server@39e678c
1 parent 428c09f commit 74fddf9

21 files changed

+205
-191
lines changed

federation-js/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
## 0.16.10
1414

1515
- The default branch of the repository has been changed to `main`. As this changed a number of references in the repository's `package.json` and `README.md` files (e.g., for badges, links, etc.), this necessitates a release to publish those changes to npm. [PR #4302](https://github.com/apollographql/apollo-server/pull/4302)
16+
- __BREAKING__: Move federation metadata from custom objects on schema nodes over to the `extensions` field on schema nodes which are intended for metadata. This is a breaking change because it narrows the `graphql` peer dependency from `^14.0.2` to `^14.5.0` which is when [`extensions` were introduced](https://github.com/graphql/graphql-js/pull/2097) for all Type System objects. [PR #4302](https://github.com/apollographql/apollo-server/pull/4313)
1617

1718
## 0.16.9
1819

federation-js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
"lodash.xorby": "^4.7.0"
2121
},
2222
"peerDependencies": {
23-
"graphql": "^14.0.2 || ^15.0.0"
23+
"graphql": "^14.5.0 || ^15.0.0"
2424
}
2525
}

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

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
selectionSetSerializer,
1212
} from '../../snapshotSerializers';
1313
import { normalizeTypeDefs } from '../normalize';
14+
import { getFederationMetadata } from '../utils';
1415

1516
expect.addSnapshotSerializer(astSerializer);
1617
expect.addSnapshotSerializer(typeSerializer);
@@ -59,8 +60,8 @@ describe('composeServices', () => {
5960
const product = schema.getType('Product') as GraphQLObjectType;
6061
const user = schema.getType('User') as GraphQLObjectType;
6162

62-
expect(product.federation.serviceName).toEqual('serviceA');
63-
expect(user.federation.serviceName).toEqual('serviceB');
63+
expect(getFederationMetadata(product).serviceName).toEqual('serviceA');
64+
expect(getFederationMetadata(user).serviceName).toEqual('serviceB');
6465
});
6566

6667
it("doesn't leave federation directives in the final schema", () => {
@@ -115,8 +116,8 @@ describe('composeServices', () => {
115116

116117
const product = schema.getType('Product') as GraphQLObjectType;
117118

118-
expect(product.federation.serviceName).toEqual('serviceA');
119-
expect(product.getFields()['price'].federation.serviceName).toEqual(
119+
expect(getFederationMetadata(product).serviceName).toEqual('serviceA');
120+
expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual(
120121
'serviceB',
121122
);
122123
});
@@ -154,8 +155,8 @@ describe('composeServices', () => {
154155

155156
const product = schema.getType('Product') as GraphQLObjectType;
156157

157-
expect(product.federation.serviceName).toEqual('serviceB');
158-
expect(product.getFields()['price'].federation.serviceName).toEqual(
158+
expect(getFederationMetadata(product).serviceName).toEqual('serviceB');
159+
expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual(
159160
'serviceA',
160161
);
161162
});
@@ -208,11 +209,11 @@ describe('composeServices', () => {
208209

209210
const product = schema.getType('Product') as GraphQLObjectType;
210211

211-
expect(product.federation.serviceName).toEqual('serviceB');
212-
expect(product.getFields()['price'].federation.serviceName).toEqual(
212+
expect(getFederationMetadata(product).serviceName).toEqual('serviceB');
213+
expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual(
213214
'serviceA',
214215
);
215-
expect(product.getFields()['color'].federation.serviceName).toEqual(
216+
expect(getFederationMetadata(product.getFields()['color']).serviceName).toEqual(
216217
'serviceC',
217218
);
218219
});
@@ -269,8 +270,8 @@ describe('composeServices', () => {
269270
}
270271
`);
271272

272-
expect(product.federation.serviceName).toEqual('serviceB');
273-
expect(product.getFields()['price'].federation.serviceName).toEqual(
273+
expect(getFederationMetadata(product).serviceName).toEqual('serviceB');
274+
expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual(
274275
'serviceC',
275276
);
276277
});
@@ -353,10 +354,10 @@ describe('composeServices', () => {
353354
name: String!
354355
}
355356
`);
356-
expect(product.getFields()['sku'].federation.serviceName).toEqual(
357+
expect(getFederationMetadata(product.getFields()['sku']).serviceName).toEqual(
357358
'serviceB',
358359
);
359-
expect(product.getFields()['name'].federation.serviceName).toEqual(
360+
expect(getFederationMetadata(product.getFields()['name']).serviceName).toEqual(
360361
'serviceB',
361362
);
362363
});
@@ -398,7 +399,7 @@ describe('composeServices', () => {
398399
name: String!
399400
}
400401
`);
401-
expect(product.getFields()['name'].federation.serviceName).toEqual(
402+
expect(getFederationMetadata(product.getFields()['name']).serviceName).toEqual(
402403
'serviceB',
403404
);
404405
});
@@ -445,7 +446,7 @@ describe('composeServices', () => {
445446
name: String!
446447
}
447448
`);
448-
expect(product.getFields()['name'].federation.serviceName).toEqual(
449+
expect(getFederationMetadata(product.getFields()['name']).serviceName).toEqual(
449450
'serviceB',
450451
);
451452
});
@@ -595,8 +596,8 @@ describe('composeServices', () => {
595596

596597
const product = schema.getType('Product') as GraphQLObjectType;
597598

598-
expect(product.federation.serviceName).toEqual('serviceA');
599-
expect(product.getFields()['id'].federation.serviceName).toEqual(
599+
expect(getFederationMetadata(product).serviceName).toEqual('serviceA');
600+
expect(getFederationMetadata(product.getFields()['id']).serviceName).toEqual(
600601
'serviceB',
601602
);
602603
});
@@ -635,7 +636,7 @@ describe('composeServices', () => {
635636

636637
const query = schema.getQueryType();
637638

638-
expect(query.federation.serviceName).toBeUndefined();
639+
expect(getFederationMetadata(query).serviceName).toBeUndefined();
639640
});
640641

641642
it('treats root Query type definition as an extension, not base definitions', () => {
@@ -676,7 +677,7 @@ describe('composeServices', () => {
676677

677678
const query = schema.getType('Query') as GraphQLObjectType;
678679

679-
expect(query.federation.serviceName).toBeUndefined();
680+
expect(getFederationMetadata(query).serviceName).toBeUndefined();
680681
});
681682

682683
it('allows extension of the Mutation type with no base type definition', () => {
@@ -806,7 +807,7 @@ describe('composeServices', () => {
806807

807808
const product = schema.getType('Product');
808809

809-
expect(product.federation.externals).toMatchInlineSnapshot(`
810+
expect(getFederationMetadata(product).externals).toMatchInlineSnapshot(`
810811
Object {
811812
"serviceB--MISSING": Array [
812813
Object {
@@ -864,10 +865,10 @@ describe('composeServices', () => {
864865
price: Int!
865866
}
866867
`);
867-
expect(product.getFields()['price'].federation.serviceName).toEqual(
868+
expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual(
868869
'serviceB',
869870
);
870-
expect(product.federation.serviceName).toEqual('serviceA');
871+
expect(getFederationMetadata(product).serviceName).toEqual('serviceA');
871872
});
872873
});
873874

@@ -897,7 +898,7 @@ describe('composeServices', () => {
897898

898899
const product = schema.getType('Product') as GraphQLObjectType;
899900
expect(
900-
product.getFields()['price'].federation.requires,
901+
getFederationMetadata(product.getFields()['price']).requires,
901902
).toMatchInlineSnapshot(`sku`);
902903
});
903904

@@ -930,7 +931,7 @@ describe('composeServices', () => {
930931
expect(errors).toHaveLength(0);
931932

932933
const product = schema.getType('Product') as GraphQLObjectType;
933-
expect(product.getFields()['price'].federation.requires)
934+
expect(getFederationMetadata(product.getFields()['price']).requires)
934935
.toMatchInlineSnapshot(`
935936
sku {
936937
id
@@ -970,7 +971,7 @@ describe('composeServices', () => {
970971
expect(errors).toHaveLength(0);
971972

972973
const review = schema.getType('Review') as GraphQLObjectType;
973-
expect(review.getFields()['product'].federation).toMatchInlineSnapshot(`
974+
expect(getFederationMetadata(review.getFields()['product'])).toMatchInlineSnapshot(`
974975
Object {
975976
"belongsToValueType": false,
976977
"provides": sku,
@@ -1013,7 +1014,7 @@ describe('composeServices', () => {
10131014
expect(errors).toHaveLength(0);
10141015

10151016
const review = schema.getType('Review') as GraphQLObjectType;
1016-
expect(review.getFields()['product'].federation.provides)
1017+
expect(getFederationMetadata(review.getFields()['product']).provides)
10171018
.toMatchInlineSnapshot(`
10181019
sku {
10191020
id
@@ -1050,7 +1051,7 @@ describe('composeServices', () => {
10501051
expect(errors).toHaveLength(0);
10511052

10521053
const review = schema.getType('Review') as GraphQLObjectType;
1053-
expect(review.getFields()['products'].federation)
1054+
expect(getFederationMetadata(review.getFields()['products']))
10541055
.toMatchInlineSnapshot(`
10551056
Object {
10561057
"belongsToValueType": false,
@@ -1099,9 +1100,9 @@ describe('composeServices', () => {
10991100
expect(errors).toHaveLength(0);
11001101

11011102
const valueType = schema.getType('ValueType') as GraphQLObjectType;
1102-
const userField = valueType.getFields()['user'].federation;
1103-
expect(userField.belongsToValueType).toBe(true);
1104-
expect(userField.serviceName).toBe(null);
1103+
const userFieldFederationMetadata = getFederationMetadata(valueType.getFields()['user']);
1104+
expect(userFieldFederationMetadata.belongsToValueType).toBe(true);
1105+
expect(userFieldFederationMetadata.serviceName).toBe(null);
11051106
});
11061107
});
11071108

@@ -1131,7 +1132,7 @@ describe('composeServices', () => {
11311132
expect(errors).toHaveLength(0);
11321133

11331134
const product = schema.getType('Product') as GraphQLObjectType;
1134-
expect(product.federation.keys).toMatchInlineSnapshot(`
1135+
expect(getFederationMetadata(product).keys).toMatchInlineSnapshot(`
11351136
Object {
11361137
"serviceA": Array [
11371138
sku,
@@ -1172,7 +1173,7 @@ describe('composeServices', () => {
11721173
expect(errors).toHaveLength(0);
11731174

11741175
const product = schema.getType('Product') as GraphQLObjectType;
1175-
expect(product.federation.keys).toMatchInlineSnapshot(`
1176+
expect(getFederationMetadata(product).keys).toMatchInlineSnapshot(`
11761177
Object {
11771178
"serviceA": Array [
11781179
color {
@@ -1215,7 +1216,7 @@ describe('composeServices', () => {
12151216
expect(errors).toHaveLength(0);
12161217

12171218
const product = schema.getType('Product') as GraphQLObjectType;
1218-
expect(product.federation.keys).toMatchInlineSnapshot(`
1219+
expect(getFederationMetadata(product).keys).toMatchInlineSnapshot(`
12191220
Object {
12201221
"serviceA": Array [
12211222
color {

federation-js/src/composition/compose.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,15 @@ import {
3131
executableDirectiveLocations,
3232
stripTypeSystemDirectivesFromTypeDefs,
3333
defaultRootOperationNameLookup,
34+
getFederationMetadata,
3435
} from './utils';
3536
import {
3637
ServiceDefinition,
3738
ExternalFieldDefinition,
3839
ServiceNameToKeyDirectivesMap,
40+
FederationType,
41+
FederationField,
42+
FederationDirective,
3943
} from './types';
4044
import { validateSDL } from 'graphql/validation/validate';
4145
import { compositionRules } from './rules';
@@ -410,13 +414,18 @@ export function addFederationMetadataToSchemaNodes({
410414
const isValueType = valueTypes.has(typeName);
411415
const serviceName = isValueType ? null : owningService;
412416

413-
namedType.federation = {
414-
...namedType.federation,
417+
const federationMetadata: FederationType = {
418+
...getFederationMetadata(namedType),
415419
serviceName,
416420
isValueType,
417421
...(keyDirectivesMap[typeName] && {
418422
keys: keyDirectivesMap[typeName],
419423
}),
424+
}
425+
426+
namedType.extensions = {
427+
...namedType.extensions,
428+
federation: federationMetadata,
420429
};
421430

422431
// For object types, add metadata for all the @provides directives from its fields
@@ -432,13 +441,18 @@ export function addFederationMetadataToSchemaNodes({
432441
providesDirective.arguments &&
433442
isStringValueNode(providesDirective.arguments[0].value)
434443
) {
435-
field.federation = {
436-
...field.federation,
444+
const fieldFederationMetadata: FederationField = {
445+
...getFederationMetadata(field),
437446
serviceName,
438447
provides: parseSelections(
439448
providesDirective.arguments[0].value.value,
440449
),
441450
belongsToValueType: isValueType,
451+
}
452+
453+
field.extensions = {
454+
...field.extensions,
455+
federation: fieldFederationMetadata
442456
};
443457
}
444458
}
@@ -455,9 +469,15 @@ export function addFederationMetadataToSchemaNodes({
455469
// TODO: Why don't we need to check for non-object types here
456470
if (isObjectType(namedType)) {
457471
const field = namedType.getFields()[fieldName];
458-
field.federation = {
459-
...field.federation,
472+
473+
const fieldFederationMetadata: FederationField = {
474+
...getFederationMetadata(field),
460475
serviceName: extendingServiceName,
476+
}
477+
478+
field.extensions = {
479+
...field.extensions,
480+
federation: fieldFederationMetadata,
461481
};
462482

463483
const [requiresDirective] = findDirectivesOnTypeOrField(
@@ -470,11 +490,16 @@ export function addFederationMetadataToSchemaNodes({
470490
requiresDirective.arguments &&
471491
isStringValueNode(requiresDirective.arguments[0].value)
472492
) {
473-
field.federation = {
474-
...field.federation,
493+
const fieldFederationMetadata: FederationField = {
494+
...getFederationMetadata(field),
475495
requires: parseSelections(
476496
requiresDirective.arguments[0].value.value,
477497
),
498+
}
499+
500+
field.extensions = {
501+
...field.extensions,
502+
federation: fieldFederationMetadata,
478503
};
479504
}
480505
}
@@ -485,31 +510,38 @@ export function addFederationMetadataToSchemaNodes({
485510
const namedType = schema.getType(field.parentTypeName);
486511
if (!namedType) continue;
487512

488-
namedType.federation = {
489-
...namedType.federation,
513+
const existingMetadata = getFederationMetadata(namedType);
514+
const typeFederationMetadata: FederationType = {
515+
...existingMetadata,
490516
externals: {
491-
...(namedType.federation && namedType.federation.externals),
517+
...existingMetadata?.externals,
492518
[field.serviceName]: [
493-
...(namedType.federation &&
494-
namedType.federation.externals &&
495-
namedType.federation.externals[field.serviceName]
496-
? namedType.federation.externals[field.serviceName]
497-
: []),
519+
...(existingMetadata?.externals?.[field.serviceName] || []),
498520
field,
499521
],
500522
},
501523
};
524+
525+
namedType.extensions = {
526+
...namedType.extensions,
527+
federation: typeFederationMetadata,
528+
};
502529
}
503530

504531
// add all definitions of a specific directive for validation later
505532
for (const directiveName of Object.keys(directiveDefinitionsMap)) {
506533
const directive = schema.getDirective(directiveName);
507534
if (!directive) continue;
508535

509-
directive.federation = {
510-
...directive.federation,
536+
const directiveFederationMetadata: FederationDirective = {
537+
...getFederationMetadata(directive),
511538
directiveDefinitions: directiveDefinitionsMap[directiveName],
512-
};
539+
}
540+
541+
directive.extensions = {
542+
...directive.extensions,
543+
federation: directiveFederationMetadata,
544+
}
513545
}
514546
}
515547

0 commit comments

Comments
 (0)