Skip to content

Commit d337489

Browse files
committed
nestjs federation compatibility
1 parent e9fe2ef commit d337489

File tree

13 files changed

+811
-17
lines changed

13 files changed

+811
-17
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM node:18
2+
WORKDIR /web
3+
COPY package.json schema.graphql tsconfig.json tsconfig.build.json ./
4+
COPY src ./src
5+
RUN npm install
6+
RUN npm run build
7+
EXPOSE 4001
8+
USER node
9+
CMD node dist/main
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
products:
3+
# must be relative to the root of the project
4+
build: implementations/nestjs
5+
ports:
6+
- 4001:4001
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "examples-nestjs-apollo-federation-compatibility",
3+
"version": "1.0.0",
4+
"description": "Apollo Federation implemented with GraphQL Yoga running as a Nest.js driver.",
5+
"private": true,
6+
"scripts": {
7+
"build": "nest build",
8+
"prebuild": "rimraf dist",
9+
"start": "nest start"
10+
},
11+
"dependencies": {
12+
"@apollo/subgraph": "^2.3.2",
13+
"@envelop/apollo-federation": "^3.0.6",
14+
"@envelop/core": "^3.0.6",
15+
"@graphql-yoga/nestjs": "0.3.1",
16+
"@nestjs/common": "^9.3.9",
17+
"@nestjs/core": "^9.3.9",
18+
"@nestjs/graphql": "^10.2.0",
19+
"graphql": "^16.6.0",
20+
"reflect-metadata": "^0.1.13",
21+
"rxjs": "^7.8.0"
22+
},
23+
"devDependencies": {
24+
"@nestjs/cli": "^9.2.0",
25+
"rimraf": "^4.1.2",
26+
"typescript": "^4.9.5"
27+
}
28+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
extend schema
2+
@link(
3+
url: "https://specs.apollo.dev/federation/v2.3"
4+
import: [
5+
"@composeDirective"
6+
"@extends"
7+
"@external"
8+
"@key"
9+
"@inaccessible"
10+
"@interfaceObject"
11+
"@override"
12+
"@provides"
13+
"@requires"
14+
"@shareable"
15+
"@tag"
16+
]
17+
)
18+
@link(url: "https://myspecs.dev/myCustomDirective/v1.0", import: ["@custom"])
19+
@composeDirective(name: "@custom")
20+
21+
directive @custom on OBJECT
22+
23+
type Product
24+
@custom
25+
@key(fields: "id")
26+
@key(fields: "sku package")
27+
@key(fields: "sku variation { id }") {
28+
id: ID!
29+
sku: String
30+
package: String
31+
variation: ProductVariation
32+
dimensions: ProductDimension
33+
createdBy: User @provides(fields: "totalProductsCreated")
34+
notes: String @tag(name: "internal")
35+
research: [ProductResearch!]!
36+
}
37+
38+
type DeprecatedProduct @key(fields: "sku package") {
39+
sku: String!
40+
package: String!
41+
reason: String
42+
createdBy: User
43+
}
44+
45+
type ProductVariation {
46+
id: ID!
47+
}
48+
49+
type ProductResearch @key(fields: "study { caseNumber }") {
50+
study: CaseStudy!
51+
outcome: String
52+
}
53+
54+
type CaseStudy {
55+
caseNumber: ID!
56+
description: String
57+
}
58+
59+
type ProductDimension @shareable {
60+
size: String
61+
weight: Float
62+
unit: String @inaccessible
63+
}
64+
65+
extend type Query {
66+
product(id: ID!): Product
67+
deprecatedProduct(sku: String!, package: String!): DeprecatedProduct
68+
@deprecated(reason: "Use product query instead")
69+
}
70+
71+
extend type User @key(fields: "email") {
72+
averageProductsCreatedPerYear: Int
73+
@requires(fields: "totalProductsCreated yearsOfEmployment")
74+
email: ID! @external
75+
name: String @override(from: "users")
76+
totalProductsCreated: Int @external
77+
yearsOfEmployment: Int! @external
78+
}
79+
80+
type Inventory @interfaceObject @key(fields: "id") {
81+
id: ID!
82+
deprecatedProducts: [DeprecatedProduct!]!
83+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
YogaFederationDriver,
3+
YogaFederationDriverConfig,
4+
} from '@graphql-yoga/nestjs'
5+
import { Module } from '@nestjs/common'
6+
import { GraphQLModule } from '@nestjs/graphql'
7+
import { DeprecatedProductsResolver } from './deprecated-products.resolver'
8+
import { InventoryResolver } from './inventory.resolver'
9+
import { ProductResearchResolver } from './product-research.resolver'
10+
import { ProductsResolver } from './products.resolver'
11+
import { UsersResolver } from './users.resolver'
12+
13+
@Module({
14+
imports: [
15+
GraphQLModule.forRoot<YogaFederationDriverConfig>({
16+
driver: YogaFederationDriver,
17+
typePaths: ['**/*.graphql'],
18+
path: '/',
19+
}),
20+
],
21+
providers: [
22+
UsersResolver,
23+
ProductsResolver,
24+
ProductResearchResolver,
25+
DeprecatedProductsResolver,
26+
InventoryResolver,
27+
],
28+
})
29+
export class AppModule {}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
Args,
3+
Query,
4+
ResolveField,
5+
Resolver,
6+
ResolveReference,
7+
} from '@nestjs/graphql'
8+
9+
interface User {
10+
email: string
11+
name: string
12+
totalProductsCreated: number
13+
}
14+
15+
interface DeprecatedProduct {
16+
sku: string
17+
package: string
18+
reason: string
19+
createdBy: User
20+
}
21+
22+
const user = {
23+
email: 'support@apollographql.com',
24+
name: 'Jane Smith',
25+
totalProductsCreated: 1337,
26+
}
27+
28+
const deprecatedProduct = {
29+
sku: 'apollo-federation-v1',
30+
package: '@apollo/federation-v1',
31+
reason: 'Migrate to Federation V2',
32+
}
33+
34+
@Resolver('DeprecatedProduct')
35+
export class DeprecatedProductsResolver {
36+
constructor() {}
37+
38+
@Query()
39+
deprecatedProduct(
40+
@Args('sku') sku: string,
41+
@Args('package') packageName: string,
42+
) {
43+
return sku === deprecatedProduct.sku &&
44+
packageName === deprecatedProduct.package
45+
? deprecatedProduct
46+
: null
47+
}
48+
49+
@ResolveField('createdBy')
50+
getCreatedBy() {
51+
return user
52+
}
53+
54+
@ResolveReference()
55+
resolveReference(reference: DeprecatedProduct) {
56+
if (
57+
reference.sku === deprecatedProduct.sku &&
58+
reference.package === deprecatedProduct.package
59+
) {
60+
return deprecatedProduct
61+
} else {
62+
return null
63+
}
64+
}
65+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Resolver, ResolveReference } from '@nestjs/graphql'
2+
3+
interface DeprecatedProduct {
4+
sku: string
5+
package: string
6+
reason: string
7+
}
8+
9+
interface Inventory {
10+
id: string
11+
deprecatedProducts: [DeprecatedProduct]
12+
}
13+
14+
const inventory = {
15+
id: 'apollo-oss',
16+
deprecatedProducts: [
17+
{
18+
sku: 'apollo-federation-v1',
19+
package: '@apollo/federation-v1',
20+
reason: 'Migrate to Federation V2',
21+
},
22+
],
23+
}
24+
25+
@Resolver('Inventory')
26+
export class InventoryResolver {
27+
constructor() {}
28+
29+
@ResolveReference()
30+
resolveReference(reference: Inventory) {
31+
if (reference.id == inventory.id) {
32+
return inventory
33+
} else {
34+
return null
35+
}
36+
}
37+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Logger } from '@nestjs/common'
2+
import { NestFactory } from '@nestjs/core'
3+
import { AppModule } from './app.module'
4+
5+
async function bootstrap() {
6+
const logger = new Logger()
7+
const app = await NestFactory.create(AppModule, { logger })
8+
await app.listen(4001)
9+
logger.log('Nest.js server listening on http://localhost:4001')
10+
}
11+
bootstrap()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ResolveField, Resolver, ResolveReference } from '@nestjs/graphql'
2+
3+
interface CaseStudy {
4+
caseNumber: string
5+
description: string
6+
}
7+
8+
interface ProductResearch {
9+
study: CaseStudy
10+
outcome: string
11+
}
12+
13+
const productResearch = [
14+
{
15+
study: {
16+
caseNumber: '1234',
17+
description: 'Federation Study',
18+
},
19+
},
20+
{
21+
study: {
22+
caseNumber: '1235',
23+
description: 'Studio Study',
24+
},
25+
},
26+
]
27+
28+
@Resolver('ProductResearch')
29+
export class ProductResearchResolver {
30+
constructor() {}
31+
32+
@ResolveField()
33+
getStudy() {
34+
return productResearch[0].study
35+
}
36+
37+
@ResolveReference()
38+
resolveReference(reference: ProductResearch) {
39+
return productResearch.find(
40+
(p) => reference.study.caseNumber === p.study.caseNumber,
41+
)
42+
}
43+
}

0 commit comments

Comments
 (0)