Skip to content

Commit 28d5294

Browse files
Merge pull request #2037 from Davide-Gheri/feature/mercurius-federation-subscriptions
feat(): mercurius federation subscriptions
2 parents 00fb1c1 + 66231c5 commit 28d5294

File tree

13 files changed

+348
-20
lines changed

13 files changed

+348
-20
lines changed

packages/apollo/tests/e2e/code-first-federation.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type Recipe implements IRecipe {
5858
description: String!
5959
}
6060
61-
type Query @extends {
61+
type Query {
6262
findPost(id: Float!): Post!
6363
getPosts: [Post!]!
6464
search: [FederationSearchResultUnion!]! @deprecated(reason: \"test\")

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

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { gql } from 'graphql-tag';
2525
import { forEach, isEmpty } from 'lodash';
2626
import { GraphQLSchemaBuilder } from '../graphql-schema.builder';
2727
import { GraphQLSchemaHost } from '../graphql-schema.host';
28-
import { GqlModuleOptions } from '../interfaces';
28+
import { GqlModuleOptions, BuildFederatedSchemaOptions } from '../interfaces';
2929
import { ResolversExplorerService, ScalarsExplorerService } from '../services';
3030
import { extend } from '../utils';
3131
import { transformSchema } from '../utils/transform-schema.util';
@@ -41,13 +41,16 @@ export class GraphQLFederationFactory {
4141

4242
async mergeWithSchema<T extends GqlModuleOptions>(
4343
options: T = {} as T,
44+
buildFederatedSchema?: (
45+
options: BuildFederatedSchemaOptions,
46+
) => GraphQLSchema,
4447
): Promise<T> {
4548
const transformSchema = async (schema: GraphQLSchema) =>
4649
options.transformSchema ? options.transformSchema(schema) : schema;
4750

4851
let schema: GraphQLSchema;
4952
if (options.autoSchemaFile) {
50-
schema = await this.generateSchema(options);
53+
schema = await this.generateSchema(options, buildFederatedSchema);
5154
} else if (isEmpty(options.typeDefs)) {
5255
schema = options.schema;
5356
} else {
@@ -81,19 +84,29 @@ export class GraphQLFederationFactory {
8184

8285
private async generateSchema<T extends GqlModuleOptions>(
8386
options: T,
87+
buildFederatedSchema?: (
88+
options: BuildFederatedSchemaOptions,
89+
) => GraphQLSchema,
8490
): Promise<GraphQLSchema> {
85-
const { buildSubgraphSchema, printSubgraphSchema } = loadPackage(
91+
const apolloSubgraph = loadPackage(
8692
'@apollo/subgraph',
8793
'ApolloFederation',
8894
() => require('@apollo/subgraph'),
8995
);
9096

97+
const printSubgraphSchema = apolloSubgraph.printSubgraphSchema;
98+
99+
if (!buildFederatedSchema) {
100+
buildFederatedSchema = apolloSubgraph.buildSubgraphSchema;
101+
}
102+
91103
const autoGeneratedSchema: GraphQLSchema = await this.buildFederatedSchema(
92104
options.autoSchemaFile,
93105
options,
94106
this.resolversExplorerService.getAllCtors(),
95107
);
96-
let executableSchema: GraphQLSchema = buildSubgraphSchema({
108+
109+
let executableSchema: GraphQLSchema = buildFederatedSchema({
97110
typeDefs: gql(printSubgraphSchema(autoGeneratedSchema)),
98111
resolvers: this.getResolvers(options.resolvers),
99112
});
@@ -170,21 +183,6 @@ export class GraphQLFederationFactory {
170183
forEach(
171184
fields,
172185
(value: GraphQLField<unknown, unknown>, key: string) => {
173-
// Workaround for https://github.com/mercurius-js/mercurius/issues/273
174-
if (key === '_service') {
175-
value.resolve = function resolve() {
176-
return {
177-
sdl: printSchema(autoGeneratedSchema)
178-
.replace('type Query {', 'type Query @extends {')
179-
.replace('type Mutation {', 'type Mutation @extends {')
180-
.replace(
181-
'type Subscription {',
182-
'type Subscription @extends {',
183-
),
184-
};
185-
};
186-
}
187-
188186
const field = autoGeneratedObjectType.getFields()[key];
189187
if (!field) {
190188
return;

packages/graphql/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ export * from './tokens';
1818
export * from './type-factories';
1919
export * from './type-helpers';
2020
export * from './utils/extend.util';
21+
export * from './utils/transform-schema.util';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { DocumentNode } from 'graphql';
2+
3+
export interface BuildFederatedSchemaOptions {
4+
typeDefs: DocumentNode;
5+
resolvers: any;
6+
}

packages/graphql/lib/interfaces/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export {
1313
export * from './graphql-driver.interface';
1414
export * from './resolve-type-fn.interface';
1515
export * from './return-type-func.interface';
16+
export * from './build-federated-schema-options.interface';

packages/mercurius/lib/drivers/mercurius-federation.driver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { printSchema } from 'graphql';
88
import { IncomingMessage, Server, ServerResponse } from 'http';
99
import mercurius from 'mercurius';
1010
import { MercuriusDriverConfig } from '../interfaces/mercurius-driver-config.interface';
11+
import { buildMercuriusFederatedSchema } from '../utils/build-mercurius-federated-schema.util';
1112

1213
@Injectable()
1314
export class MercuriusFederationDriver extends AbstractGraphQLDriver<MercuriusDriverConfig> {
@@ -29,6 +30,7 @@ export class MercuriusFederationDriver extends AbstractGraphQLDriver<MercuriusDr
2930
public async start(options: MercuriusDriverConfig) {
3031
const adapterOptions = await this.graphqlFederationFactory.mergeWithSchema(
3132
options,
33+
buildMercuriusFederatedSchema,
3234
);
3335

3436
if (adapterOptions.definitions && adapterOptions.definitions.path) {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { loadPackage } from '@nestjs/common/utils/load-package.util';
2+
import { transformSchema } from '@nestjs/graphql';
3+
import { BuildFederatedSchemaOptions } from '@nestjs/graphql';
4+
import { GraphQLSchema, isObjectType, buildASTSchema } from 'graphql';
5+
import { forEach } from 'lodash';
6+
7+
export function buildMercuriusFederatedSchema({
8+
typeDefs,
9+
resolvers,
10+
}: BuildFederatedSchemaOptions) {
11+
const { buildSubgraphSchema, printSubgraphSchema } = loadPackage(
12+
'@apollo/subgraph',
13+
'MercuriusFederation',
14+
() => require('@apollo/subgraph'),
15+
);
16+
let executableSchema: GraphQLSchema = buildSubgraphSchema({
17+
typeDefs,
18+
resolvers,
19+
});
20+
21+
const subscriptionResolvers = resolvers.Subscription;
22+
executableSchema = transformSchema(executableSchema, (type) => {
23+
if (isObjectType(type)) {
24+
const isSubscription = type.name === 'Subscription';
25+
forEach(type.getFields(), (value, key) => {
26+
if (isSubscription && subscriptionResolvers) {
27+
const resolver = subscriptionResolvers[key];
28+
if (resolver && !value.subscribe) {
29+
value.subscribe = resolver.subscribe;
30+
}
31+
} else if (key === '_service') {
32+
// Workaround for https://github.com/mercurius-js/mercurius/issues/273
33+
value.resolve = function resolve() {
34+
return {
35+
sdl: printSubgraphSchema(
36+
buildASTSchema(typeDefs, {
37+
assumeValid: true,
38+
}),
39+
)
40+
.replace('type Query {', 'type Query @extends {')
41+
.replace('type Mutation {', 'type Mutation @extends {')
42+
.replace('type Subscription {', 'type Subscription @extends {'),
43+
};
44+
};
45+
}
46+
});
47+
}
48+
return type;
49+
});
50+
51+
return executableSchema;
52+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Module } from '@nestjs/common';
2+
import { DynamicModule } from '@nestjs/common/interfaces';
3+
import { GraphQLModule } from '@nestjs/graphql';
4+
import { MercuriusFederationDriver, MercuriusFederationDriverConfig } from '../../../lib';
5+
import { NotificationModule } from './notification.module';
6+
7+
export type AppModuleConfig = {
8+
context?: MercuriusFederationDriverConfig['context'];
9+
subscription?: MercuriusFederationDriverConfig['subscription'];
10+
};
11+
12+
@Module({})
13+
export class AppModule {
14+
static forRoot(options?: AppModuleConfig): DynamicModule {
15+
return {
16+
module: AppModule,
17+
imports: [
18+
NotificationModule,
19+
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
20+
driver: MercuriusFederationDriver,
21+
context: options?.context,
22+
federationMetadata: true,
23+
autoSchemaFile: true,
24+
subscription: options?.subscription,
25+
}),
26+
],
27+
};
28+
}
29+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2+
import { GqlExecutionContext } from '@nestjs/graphql';
3+
4+
@Injectable()
5+
export class AuthGuard implements CanActivate {
6+
canActivate(context: ExecutionContext): boolean {
7+
const ctx = GqlExecutionContext.create(context).getContext();
8+
return !!ctx.user?.startsWith?.('test');
9+
}
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Module } from '@nestjs/common';
2+
import { NotificationResolver } from './notification.resolver';
3+
4+
@Module({
5+
providers: [NotificationResolver],
6+
})
7+
export class NotificationModule {}

0 commit comments

Comments
 (0)