Skip to content

Commit b524322

Browse files
refactor(): make federation config more extensible, scalable
1 parent c4d1123 commit b524322

File tree

11 files changed

+143
-79
lines changed

11 files changed

+143
-79
lines changed

packages/apollo/tests/code-first-graphql-federation2/posts-service/federation-posts.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { User } from './posts/user.entity';
1111
GraphQLModule.forRoot<ApolloDriverConfig>({
1212
driver: ApolloFederationDriver,
1313
autoSchemaFile: {
14-
useFed2: true,
14+
federation: 2,
1515
},
1616
buildSchemaOptions: {
1717
orphanedTypes: [User],

packages/apollo/tests/code-first-graphql-federation2/users-service/federation-users.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { UsersModule } from './users/users.module';
1010
GraphQLModule.forRoot<ApolloDriverConfig>({
1111
driver: ApolloFederationDriver,
1212
autoSchemaFile: {
13-
useFed2: true,
13+
federation: 2,
1414
},
1515
plugins: [ApolloServerPluginInlineTraceDisabled()],
1616
}),

packages/apollo/tests/jest-e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const config: Config.InitialOptions = {
1010
moduleFileExtensions: ['js', 'json', 'ts'],
1111
rootDir: '../.',
1212
testRegex: '.spec.ts$',
13-
testPathIgnorePatterns: ['.fed2-spec.ts$'],
13+
testPathIgnorePatterns: ['.fed([1-9]).spec.ts$'],
1414
moduleNameMapper,
1515
transform: {
1616
'^.+\\.(t|j)s$': 'ts-jest',

packages/graphql/lib/federation/graphql-federation.factory.ts

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { mergeSchemas } from '@graphql-tools/schema';
22
import { printSchemaWithDirectives } from '@graphql-tools/utils';
3-
import { Injectable, Logger } from '@nestjs/common';
3+
import { Injectable } from '@nestjs/common';
44
import { loadPackage } from '@nestjs/common/utils/load-package.util';
55
import { isString } from '@nestjs/common/utils/shared.utils';
66
import {
@@ -27,13 +27,18 @@ import { forEach, isEmpty } from 'lodash';
2727
import { GraphQLSchemaBuilder } from '../graphql-schema.builder';
2828
import { GraphQLSchemaHost } from '../graphql-schema.host';
2929
import {
30-
GqlModuleOptions,
31-
BuildFederatedSchemaOptions,
3230
AutoSchemaFileValue,
31+
BuildFederatedSchemaOptions,
32+
FederationConfig,
33+
FederationVersion,
34+
GqlModuleOptions,
3335
} from '../interfaces';
3436
import { ResolversExplorerService, ScalarsExplorerService } from '../services';
35-
import { extend, getFederation2Info, stringifyWithoutQuotes } from '../utils';
37+
import { extend } from '../utils';
3638
import { transformSchema } from '../utils/transform-schema.util';
39+
import { TypeDefsDecoratorFactory } from './type-defs-decorator.factory';
40+
41+
const DEFAULT_FEDERATION_VERSION: FederationVersion = 1;
3742

3843
@Injectable()
3944
export class GraphQLFederationFactory {
@@ -42,6 +47,7 @@ export class GraphQLFederationFactory {
4247
private readonly scalarsExplorerService: ScalarsExplorerService,
4348
private readonly gqlSchemaBuilder: GraphQLSchemaBuilder,
4449
private readonly gqlSchemaHost: GraphQLSchemaHost,
50+
private readonly typeDefsDecoratorFactory: TypeDefsDecoratorFactory,
4551
) {}
4652

4753
async mergeWithSchema<T extends GqlModuleOptions>(
@@ -102,7 +108,9 @@ export class GraphQLFederationFactory {
102108
await import('@apollo/subgraph/package.json')
103109
).version;
104110

105-
const isApolloSubgraph2 = Number(apolloSubgraphVersion.split('.')[0]) >= 2;
111+
const apolloSubgraphMajorVersion = Number(
112+
apolloSubgraphVersion.split('.')[0],
113+
);
106114
const printSubgraphSchema = apolloSubgraph.printSubgraphSchema;
107115

108116
if (!buildFederatedSchema) {
@@ -114,45 +122,20 @@ export class GraphQLFederationFactory {
114122
options,
115123
this.resolversExplorerService.getAllCtors(),
116124
);
117-
let typeDefs = isApolloSubgraph2
118-
? printSchemaWithDirectives(autoGeneratedSchema)
119-
: printSubgraphSchema(autoGeneratedSchema);
125+
let typeDefs =
126+
apolloSubgraphMajorVersion >= 2
127+
? printSchemaWithDirectives(autoGeneratedSchema)
128+
: printSubgraphSchema(autoGeneratedSchema);
120129

121-
const useFed2 = getFederation2Info(options.autoSchemaFile);
130+
const [federationVersion, federationOptions] =
131+
this.getFederationVersionAndConfig(options.autoSchemaFile);
122132

123-
if (useFed2 && isApolloSubgraph2) {
124-
const {
125-
directives = [
126-
'@key',
127-
'@shareable',
128-
'@external',
129-
'@override',
130-
'@requires',
131-
],
132-
importUrl = 'https://specs.apollo.dev/federation/v2.0',
133-
} = typeof useFed2 === 'boolean' ? {} : useFed2;
134-
const mappedDirectives = directives
135-
.map((directive) => {
136-
if (!isString(directive)) {
137-
return stringifyWithoutQuotes(directive);
138-
}
139-
let finalDirective = directive;
140-
if (!directive.startsWith('@')) {
141-
finalDirective = `@${directive}`;
142-
}
143-
return `"${finalDirective}"`;
144-
})
145-
.join(', ');
146-
147-
typeDefs = `
148-
extend schema @link(url: "${importUrl}", import: [${mappedDirectives}])
149-
${typeDefs}
150-
`;
151-
} else if (useFed2 && !isApolloSubgraph2) {
152-
Logger.error(
153-
'You are trying to use Apollo Federation 2 but you are not using @apollo/subgraph@^2.0.0, please upgrade',
154-
'GraphQLFederationFactory',
155-
);
133+
const typeDefsDecorator = this.typeDefsDecoratorFactory.create(
134+
federationVersion,
135+
apolloSubgraphMajorVersion,
136+
);
137+
if (typeDefsDecorator) {
138+
typeDefs = typeDefsDecorator.decorate(typeDefs, federationOptions);
156139
}
157140

158141
let executableSchema: GraphQLSchema = buildFederatedSchema({
@@ -341,17 +324,23 @@ export class GraphQLFederationFactory {
341324
const scalarsMap = this.scalarsExplorerService.getScalarsMap();
342325
try {
343326
const buildSchemaOptions = options.buildSchemaOptions || {};
344-
const useFed2 = getFederation2Info(options.autoSchemaFile);
327+
const directives = [...specifiedDirectives];
328+
const [federationVersion] =
329+
this.getFederationVersionAndConfig(autoSchemaFile);
330+
331+
if (federationVersion < 2) {
332+
directives.push(...this.loadFederationDirectives());
333+
}
334+
if (buildSchemaOptions?.directives) {
335+
directives.push(...buildSchemaOptions.directives);
336+
}
337+
345338
return await this.gqlSchemaBuilder.generateSchema(
346339
resolvers,
347340
autoSchemaFile,
348341
{
349342
...buildSchemaOptions,
350-
directives: [
351-
...specifiedDirectives,
352-
...(useFed2 ? [] : this.loadFederationDirectives()),
353-
...((buildSchemaOptions && buildSchemaOptions.directives) || []),
354-
],
343+
directives,
355344
scalarsMap,
356345
skipCheck: true,
357346
},
@@ -366,6 +355,21 @@ export class GraphQLFederationFactory {
366355
}
367356
}
368357

358+
private getFederationVersionAndConfig(
359+
autoSchemaFile: AutoSchemaFileValue,
360+
): [FederationVersion, FederationConfig?] {
361+
if (!autoSchemaFile || typeof autoSchemaFile !== 'object') {
362+
return [DEFAULT_FEDERATION_VERSION];
363+
}
364+
if (typeof autoSchemaFile.federation !== 'object') {
365+
return [autoSchemaFile.federation ?? DEFAULT_FEDERATION_VERSION];
366+
}
367+
return [
368+
autoSchemaFile.federation?.version ?? DEFAULT_FEDERATION_VERSION,
369+
autoSchemaFile.federation,
370+
];
371+
}
372+
369373
private loadFederationDirectives() {
370374
const { federationDirectives, directivesWithNoDefinitionNeeded } =
371375
loadPackage('@apollo/subgraph/dist/directives', 'SchemaBuilder', () =>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { FederationConfig, FederationVersion } from '../interfaces';
3+
import { TypeDefsFederation2Decorator } from './type-defs-federation2.decorator';
4+
5+
export interface TypeDefsDecorator<T = FederationConfig> {
6+
decorate(typeDefs: string, options: T): string;
7+
}
8+
9+
@Injectable()
10+
export class TypeDefsDecoratorFactory {
11+
private readonly logger = new Logger(TypeDefsDecoratorFactory.name);
12+
13+
create(
14+
federationVersion: FederationVersion,
15+
apolloSubgraphVersion: number,
16+
): TypeDefsDecorator | undefined {
17+
switch (federationVersion) {
18+
case 2: {
19+
if (apolloSubgraphVersion === 1) {
20+
this.logger.error(
21+
'You are trying to use Apollo Federation 2 but you are not using @apollo/subgraph@^2.0.0, please upgrade',
22+
);
23+
return;
24+
}
25+
return new TypeDefsFederation2Decorator();
26+
}
27+
default:
28+
return;
29+
}
30+
}
31+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { isString } from '@nestjs/common/utils/shared.utils';
2+
import { Federation2Config } from '../interfaces';
3+
import { stringifyWithoutQuotes } from '../utils';
4+
5+
export class TypeDefsFederation2Decorator {
6+
decorate(typeDefs: string, config: Federation2Config = { version: 2 }) {
7+
const {
8+
directives = [
9+
'@key',
10+
'@shareable',
11+
'@external',
12+
'@override',
13+
'@requires',
14+
],
15+
importUrl = 'https://specs.apollo.dev/federation/v2.0',
16+
} = config;
17+
const mappedDirectives = directives
18+
.map((directive) => {
19+
if (!isString(directive)) {
20+
return stringifyWithoutQuotes(directive);
21+
}
22+
let finalDirective = directive;
23+
if (!directive.startsWith('@')) {
24+
finalDirective = `@${directive}`;
25+
}
26+
return `"${finalDirective}"`;
27+
})
28+
.join(', ');
29+
30+
return `
31+
extend schema @link(url: "${importUrl}", import: [${mappedDirectives}])
32+
${typeDefs}
33+
`;
34+
}
35+
}

packages/graphql/lib/graphql.module.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import {
66
Provider,
77
} from '@nestjs/common/interfaces';
88
import { HttpAdapterHost } from '@nestjs/core';
9-
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
109
import { ROUTE_MAPPED_MESSAGE } from '@nestjs/core/helpers/messages';
10+
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
1111
import { AbstractGraphQLDriver } from './drivers/abstract-graphql.driver';
1212
import { GraphQLFederationFactory } from './federation/graphql-federation.factory';
13+
import { TypeDefsDecoratorFactory } from './federation/type-defs-decorator.factory';
1314
import { GraphQLAstExplorer } from './graphql-ast.explorer';
1415
import { GraphQLSchemaBuilder } from './graphql-schema.builder';
1516
import { GraphQLSchemaHost } from './graphql-schema.host';
@@ -37,6 +38,7 @@ import { extend, generateString } from './utils';
3738
GraphQLSchemaBuilder,
3839
GraphQLSchemaHost,
3940
GraphQLFederationFactory,
41+
TypeDefsDecoratorFactory,
4042
],
4143
exports: [
4244
GraphQLTypesLoader,
@@ -49,7 +51,9 @@ export class GraphQLModule<
4951
TAdapter extends AbstractGraphQLDriver = AbstractGraphQLDriver,
5052
> implements OnModuleInit, OnModuleDestroy
5153
{
52-
private static readonly logger = new Logger(GraphQLModule.name, { timestamp: true });
54+
private static readonly logger = new Logger(GraphQLModule.name, {
55+
timestamp: true,
56+
});
5357

5458
get graphQlAdapter(): TAdapter {
5559
return this._graphQlAdapter as TAdapter;
@@ -160,7 +164,9 @@ export class GraphQLModule<
160164
});
161165

162166
if (options.path) {
163-
GraphQLModule.logger.log(ROUTE_MAPPED_MESSAGE(options.path, RequestMethod.POST));
167+
GraphQLModule.logger.log(
168+
ROUTE_MAPPED_MESSAGE(options.path, RequestMethod.POST),
169+
);
164170
}
165171
}
166172

packages/graphql/lib/interfaces/schema-file-config.interface.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ export interface AliasDirectiveImport {
33
as: string;
44
}
55

6+
export type FederationVersion = 1 | 2;
7+
export type FederationConfig = Federation2Config;
8+
69
export interface Federation2Config {
10+
version: 2;
711
/**
812
* The imported directives
913
* @default ['@key', '@shareable', '@external', '@override', '@requires']
@@ -16,23 +20,16 @@ export interface Federation2Config {
1620
importUrl?: string;
1721
}
1822

19-
export type UseFed2Value = boolean | Federation2Config;
20-
2123
export interface SchemaFileConfig {
2224
/**
23-
* If enabled, it will use federation 2 schema
24-
*
25-
* **Note:** You need to have installed @apollo/subgraph@^2.0.0 and enable `autoSchemaFile`
26-
*
27-
* This will add to your schema:
28-
* ```graphql
29-
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable", "@external", "@override", "@requires"])
30-
* ```
31-
*/
32-
useFed2?: UseFed2Value;
25+
* Federation version and its configuration,
26+
*
27+
* @default 1
28+
*/
29+
federation?: FederationVersion | FederationConfig;
3330

3431
/**
35-
* The path to the schema file
32+
* Path to the schema file.
3633
*/
3734
path?: string;
3835
}
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { isString, isObject } from '@nestjs/common/utils/shared.utils';
2-
import { AutoSchemaFileValue, UseFed2Value } from '../interfaces';
1+
import { isObject, isString } from '@nestjs/common/utils/shared.utils';
2+
import { AutoSchemaFileValue } from '../interfaces';
33

44
export function getPathForAutoSchemaFile(
55
autoSchemaFile: AutoSchemaFileValue,
@@ -12,12 +12,3 @@ export function getPathForAutoSchemaFile(
1212
}
1313
return null;
1414
}
15-
16-
export function getFederation2Info(
17-
autoSchemaFile: AutoSchemaFileValue,
18-
): UseFed2Value {
19-
if (isObject(autoSchemaFile)) {
20-
return autoSchemaFile.useFed2;
21-
}
22-
return false;
23-
}

packages/graphql/lib/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ export * from './extract-metadata.util';
44
export * from './generate-token.util';
55
export * from './get-number-of-arguments.util';
66
export * from './normalize-route-path.util';
7-
export * from './object.util';
87
export * from './remove-temp.util';
8+
export * from './stringify-without-quotes.util';

0 commit comments

Comments
 (0)