Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions sample/31-graphql-federation-code-first/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
# GraphQL Federation - Code First

A simple example of GraphQL Federation using Code First approach.
A production-ready example of GraphQL Federation using Code First approach.

## Installation

Install dependencies for all applications:

```sh
cd users-application && npm install
cd ../posts-application && npm install
cd ../gateway && npm install
```

## Execution

Make sure to start the two sub-graph applications first, then the gateway. Otherwise the gateway won't be able to fetch schemas from the sub-graphs.
### Development Mode

For development, the gateway will automatically generate a supergraph schema on startup:

```sh
# Start sub-graph applications first
cd users-application && npm run start
```

Expand All @@ -15,9 +28,39 @@ cd posts-application && npm run start
```

```sh
# The gateway will auto-generate supergraph.graphql on startup
cd gateway && npm run start
```

### Production Mode

For production, generate the supergraph schema beforehand:

```sh
# 1. Ensure sub-graphs are running
cd users-application && npm run start
cd ../posts-application && npm run start

# 2. Generate supergraph schema using Rover CLI (requires @apollo/rover installation)
cd ../gateway && npm run generate:supergraph:rover

# 3. Start the gateway with the static supergraph
npm run start:prod
```

## Supergraph Schema Generation

The gateway provides two methods for generating the supergraph schema:

1. **Local Generation (Development)**: `npm run generate:supergraph`
- Generates a simplified supergraph for local development
- Does not require Rover CLI

2. **Rover CLI Generation (Production)**: `npm run generate:supergraph:rover`
- Uses Apollo Rover to compose the actual supergraph from running sub-graphs
- Requires Rover CLI: `npm install -g @apollo/rover`
- Recommended for production deployments

## Access the graph

You can reach the gateway under `http://localhost:3001/graphql`
Expand Down
5 changes: 4 additions & 1 deletion sample/31-graphql-federation-code-first/gateway/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ lerna-debug.log*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/extensions.json

# Generated files
supergraph.graphql
114 changes: 114 additions & 0 deletions sample/31-graphql-federation-code-first/gateway/generate-supergraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';

const execAsync = promisify(exec);

async function generateSupergraph() {
try {
// Check if Rover CLI is installed
try {
await execAsync('rover --version');
} catch (error) {
console.error('Rover CLI is not installed. Please install it first:');
console.error('npm install -g @apollo/rover');
process.exit(1);
}

// Generate supergraph schema
console.log('Generating supergraph schema...');
const { stdout, stderr } = await execAsync(
'rover supergraph compose --config ./supergraph.yaml',
);

if (stderr && !stderr.includes('WARN')) {
console.error('Error generating supergraph:', stderr);
process.exit(1);
}

// Write the supergraph schema to a file
writeFileSync(join(__dirname, 'supergraph.graphql'), stdout);
console.log('Supergraph schema generated successfully!');
console.log('Output written to: supergraph.graphql');
} catch (error) {
console.error('Failed to generate supergraph:', error);
process.exit(1);
}
}

// For local development without Rover
async function generateSupergraphLocal() {
console.log('Generating local supergraph schema for development...');

// This is a simplified supergraph for local development
// In production, use Rover CLI to generate the actual supergraph
const supergraphSchema = `
schema
@link(url: "https://specs.apollo.dev/federation/v2.3")
@link(url: "https://specs.apollo.dev/link/v1.0")
{
query: Query
}

directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA

directive @key(fields: String!, resolvable: Boolean = true) on OBJECT | INTERFACE

directive @requires(fields: String!) on FIELD_DEFINITION

directive @provides(fields: String!) on FIELD_DEFINITION

directive @external on OBJECT | FIELD_DEFINITION

directive @shareable on OBJECT | FIELD_DEFINITION

directive @extends on OBJECT | INTERFACE

scalar link__Import

enum link__Purpose {
SECURITY
EXECUTION
}

type Query {
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
}

union _Entity = User | Post

scalar _Any

type _Service {
sdl: String!
}

type User @key(fields: "id") {
id: ID!
name: String!
posts: [Post!]!
}

type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
authorId: ID!
user: User!
}
`;

writeFileSync(join(__dirname, 'supergraph.graphql'), supergraphSchema);
console.log('Local supergraph schema generated successfully!');
}

// Check command line arguments
const useRover = process.argv.includes('--rover');

if (useRover) {
generateSupergraph();
} else {
generateSupergraphLocal();
}
4 changes: 4 additions & 0 deletions sample/31-graphql-federation-code-first/gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"generate:supergraph": "ts-node generate-supergraph.ts",
"generate:supergraph:rover": "ts-node generate-supergraph.ts --rover",
"prestart": "npm run generate:supergraph",
"prestart:dev": "npm run generate:supergraph",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
Expand Down
25 changes: 18 additions & 7 deletions sample/31-graphql-federation-code-first/gateway/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import { IntrospectAndCompose } from '@apollo/gateway';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { readFileSync } from 'fs';
import { join } from 'path';

// For development, you can use IntrospectAndCompose (not recommended for production)
// import { IntrospectAndCompose } from '@apollo/gateway';

const supergraphSdl = readFileSync(
join(__dirname, '..', 'supergraph.graphql'),
'utf-8',
).trim();

@Module({
imports: [
GraphQLModule.forRoot<ApolloGatewayDriverConfig>({
driver: ApolloGatewayDriver,
gateway: {
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:3002/graphql' },
{ name: 'posts', url: 'http://localhost:3003/graphql' },
],
}),
supergraphSdl,
// For development only - not recommended for production
// supergraphSdl: new IntrospectAndCompose({
// subgraphs: [
// { name: 'users', url: 'http://localhost:3002/graphql' },
// { name: 'posts', url: 'http://localhost:3003/graphql' },
// ],
// }),
},
}),
],
Expand Down
10 changes: 10 additions & 0 deletions sample/31-graphql-federation-code-first/gateway/supergraph.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
federation_version: =2.3.2
subgraphs:
users:
routing_url: http://localhost:3002/graphql
schema:
subgraph_url: http://localhost:3002/graphql
posts:
routing_url: http://localhost:3003/graphql
schema:
subgraph_url: http://localhost:3003/graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { existsSync } from 'fs';
import { join } from 'path';

describe('GraphQL Federation Gateway (Code First)', () => {
let app: INestApplication;

beforeAll(async () => {
// Ensure supergraph.graphql exists
const supergraphPath = join(__dirname, '..', 'supergraph.graphql');
if (!existsSync(supergraphPath)) {
// Generate a default supergraph for testing
require('../generate-supergraph');
}

const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

afterAll(async () => {
await app.close();
});

it('should load gateway with static supergraph', () => {
return request(app.getHttpServer())
.post('/graphql')
.send({
query: `
query {
_service {
sdl
}
}
`,
})
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.data._service).toBeDefined();
expect(res.body.data._service.sdl).toContain('type User');
expect(res.body.data._service.sdl).toContain('type Post');
});
});

it('should not use IntrospectAndCompose in production', () => {
const appModulePath = join(__dirname, '..', 'src', 'app.module.ts');
const fs = require('fs');
const appModuleContent = fs.readFileSync(appModulePath, 'utf-8');

// Check that IntrospectAndCompose is commented out or not used
const hasActiveIntrospect =
appModuleContent.includes('new IntrospectAndCompose') &&
!appModuleContent.includes('// supergraphSdl: new IntrospectAndCompose');

expect(hasActiveIntrospect).toBe(false);
});

it('should read supergraph from file system', () => {
const appModulePath = join(__dirname, '..', 'src', 'app.module.ts');
const fs = require('fs');
const appModuleContent = fs.readFileSync(appModulePath, 'utf-8');

// Check that the module reads from a file
expect(appModuleContent).toContain('readFileSync');
expect(appModuleContent).toContain('supergraph.graphql');
});
});
Loading
Loading