Skip to content

Commit f47adbc

Browse files
committed
feat(federation): add TypeGraphQLFederationModule
1 parent bcb434c commit f47adbc

10 files changed

Lines changed: 341 additions & 74 deletions

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,57 @@ Example of using the config service to generate `TypeGraphQLModule` options:
126126
export default class AppModule {}
127127
```
128128

129+
### `TypeGraphQLFederationModule`
130+
131+
`typegraphql-nestjs` has also support for [Apollo Federation](https://www.apollographql.com/docs/federation/).
132+
133+
However, Apollo Federation requires building a federated GraphQL schema, hence you need to use the `TypeGraphQLFederationModule` module, designed specially for that case.
134+
135+
The usage is really similar to the basic `TypeGraphQLModule` - the only different is that `.forFeature()` method has an option to provide `referenceResolvers` object which is needed in some cases of Apollo Federation:
136+
137+
```ts
138+
function resolveUserReference(
139+
reference: Pick<User, "id">,
140+
): Promise<User | undefined> {
141+
return db.users.find({ id: reference.id });
142+
}
143+
144+
@Module({
145+
imports: [
146+
TypeGraphQLFederationModule.forFeature({
147+
orphanedTypes: [User],
148+
referenceResolvers: {
149+
User: {
150+
__resolveReference: resolveUserReference,
151+
},
152+
},
153+
}),
154+
],
155+
providers: [AccountsResolver],
156+
})
157+
export default class AccountModule {}
158+
```
159+
160+
The `.forRoot()` method has no differences but you should provide the `skipCheck: true` option as federated schema can violate the standard GraphQL schema rules like at least one query defined:
161+
162+
```ts
163+
@Module({
164+
imports: [
165+
TypeGraphQLFederationModule.forRoot({
166+
validate: false,
167+
skipCheck: true,
168+
}),
169+
AccountModule,
170+
],
171+
})
172+
export default class AppModule {}
173+
```
174+
175+
> Be aware that you cannot mix `TypeGraphQLFederationModule.forRoot()` with the base `TypeGraphQLModule.forFeature()` one.
176+
> You need to consistently use only `TypeGraphQLFederationModule` across all modules.
177+
178+
Then, for exposing the federated schema using Apollo Gateway, you should use the standard NestJS [GraphQLGatewayModule](https://docs.nestjs.com/graphql/federation#federated-example-gateway).
179+
129180
## Caveats
130181

131182
While this integration provides a way to use TypeGraphQL with NestJS modules and dependency injector, for now it doesn't support [other NestJS features](https://docs.nestjs.com/graphql/tooling) like guards, interceptors, filters and pipes.
@@ -150,6 +201,10 @@ You can see some examples of the integration in this repo:
150201

151202
Usage of request scoped dependencies - retrieving fresh instances of resolver and service classes on every request (query/mutation)
152203

204+
1. [Apollo Federation](https://github.com/MichalLytek/typegraphql-nestjs/tree/master/examples/4-federation)
205+
206+
Showcase of Apollo Federation approach, using the `TypeGraphQLFederationModule` and `GraphQLGatewayModule`.
207+
153208
You can run them by using `ts-node`, like `npx ts-node ./examples/1-basics/index.ts`.
154209

155210
All examples folders contain a `query.gql` file with some examples operations you can perform on the GraphQL servers.

package-lock.json

Lines changed: 24 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,31 @@
1111
"test": "jest"
1212
},
1313
"peerDependencies": {
14+
"@apollo/federation": "^0.17.0",
1415
"@nestjs/common": "^7.6.13",
1516
"@nestjs/core": "^7.6.13",
1617
"@nestjs/graphql": "^7.9.10",
18+
"apollo-graphql": "^0.4.5",
19+
"graphql-tag": "^2.12.1",
1720
"type-graphql": "^1.1.1"
1821
},
1922
"dependencies": {
2023
"tslib": "^2.1.0"
2124
},
2225
"devDependencies": {
26+
"@apollo/federation": "^0.17.0",
2327
"@nestjs/common": "^7.6.13",
2428
"@nestjs/core": "^7.6.13",
2529
"@nestjs/graphql": "^7.9.10",
2630
"@nestjs/platform-fastify": "^7.6.13",
2731
"@nestjs/testing": "^7.6.13",
2832
"@types/jest": "^26.0.20",
2933
"@types/node": "^14.14.31",
34+
"apollo-graphql": "^0.4.5",
3035
"apollo-server-fastify": "^2.21.0",
3136
"class-validator": "^0.13.1",
3237
"graphql": "^15.5.0",
38+
"graphql-tag": "^2.12.1",
3339
"graphql-tools": "^7.0.4",
3440
"husky": "^4.3.8",
3541
"jest": "^26.6.3",

src/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,9 @@ export const TYPEGRAPHQL_ROOT_MODULE_OPTIONS =
33

44
export const TYPEGRAPHQL_FEATURE_MODULE_OPTIONS =
55
"TYPEGRAPHQL_FEATURE_MODULE_OPTIONS";
6+
7+
export const TYPEGRAPHQL_ROOT_FEDERATION_MODULE_OPTIONS =
8+
"TYPEGRAPHQL_ROOT_FEDERATION_MODULE_OPTIONS";
9+
10+
export const TYPEGRAPHQL_FEATURE_FEDERATION_MODULE_OPTIONS =
11+
"TYPEGRAPHQL_FEATURE_FEDERATION_MODULE_OPTIONS";

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./types";
22
export { TypeGraphQLModule } from "./typegraphql.module";
3+
export { TypeGraphQLFederationModule } from "./typegraphql-federation.module";

src/prepare-options.service.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Injectable, flatten } from "@nestjs/common";
2+
import { ModulesContainer, ModuleRef, ContextIdFactory } from "@nestjs/core";
3+
import { REQUEST_CONTEXT_ID } from "@nestjs/core/router/request/request-constants";
4+
import { InstanceWrapper } from "@nestjs/core/injector/instance-wrapper";
5+
import { ClassType, ContainerType, getMetadataStorage } from "type-graphql";
6+
7+
import { TypeGraphQLFeatureModuleOptions } from "./types";
8+
9+
@Injectable()
10+
export default class OptionsPreparatorService {
11+
constructor(
12+
private readonly moduleRef: ModuleRef,
13+
private readonly modulesContainer: ModulesContainer,
14+
) {}
15+
16+
prepareOptions<TOptions extends TypeGraphQLFeatureModuleOptions>(
17+
featureModuleToken: string,
18+
) {
19+
const globalResolvers = getMetadataStorage().resolverClasses.map(
20+
metadata => metadata.target,
21+
);
22+
23+
const featureModuleOptionsArray: TOptions[] = [];
24+
const resolversClasses: ClassType[] = [];
25+
const providersMetadataMap = new Map<Function, InstanceWrapper<any>>();
26+
27+
for (const module of this.modulesContainer.values()) {
28+
for (const provider of module.providers.values()) {
29+
if (
30+
typeof provider.name === "string" &&
31+
provider.name.includes(featureModuleToken)
32+
) {
33+
featureModuleOptionsArray.push(provider.instance as TOptions);
34+
}
35+
if (globalResolvers.includes(provider.metatype)) {
36+
providersMetadataMap.set(provider.metatype, provider);
37+
resolversClasses.push(provider.metatype as ClassType);
38+
}
39+
}
40+
}
41+
42+
const orphanedTypes = flatten(
43+
featureModuleOptionsArray.map(it => it.orphanedTypes),
44+
);
45+
const container: ContainerType = {
46+
get: (cls, { context }) => {
47+
let contextId = context[REQUEST_CONTEXT_ID];
48+
if (!contextId) {
49+
contextId = ContextIdFactory.create();
50+
context[REQUEST_CONTEXT_ID] = contextId;
51+
}
52+
const providerMetadata = providersMetadataMap.get(cls)!;
53+
if (
54+
providerMetadata.isDependencyTreeStatic() &&
55+
!providerMetadata.isTransient
56+
) {
57+
return this.moduleRef.get(cls, { strict: false });
58+
}
59+
return this.moduleRef.resolve(cls, contextId, { strict: false });
60+
},
61+
};
62+
63+
return {
64+
resolversClasses,
65+
orphanedTypes,
66+
container,
67+
featureModuleOptionsArray,
68+
};
69+
}
70+
}

0 commit comments

Comments
 (0)