Skip to content

Commit 36aea5e

Browse files
committed
feat(): durable providers #1 - multi-tenancy
1 parent 2c965f7 commit 36aea5e

13 files changed

+216
-1
lines changed

src/app.module.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import { FibonacciModule } from './fibonacci/fibonacci.module'
99
import { HttpClientModule } from './http-client/http-client.module'
1010
import { TagsModule } from './tags/tags.module'
1111
import { EventEmitterModule } from '@nestjs/event-emitter'
12-
import { PaymentsModule } from './payments/payments.module';
12+
import { PaymentsModule } from './payments/payments.module'
13+
import { DataSourceModule } from './data-source/data-source.module'
14+
import { UsersModule } from './users/users.module'
15+
import { ContextIdFactory } from '@nestjs/core'
16+
import { AggregateByTenantContextIdStrategy } from './core/aggregate-by-tenant.strategy'
17+
18+
ContextIdFactory.apply(new AggregateByTenantContextIdStrategy())
1319

1420
@Module({
1521
imports: [
@@ -29,6 +35,8 @@ import { PaymentsModule } from './payments/payments.module';
2935
}),
3036
TagsModule,
3137
PaymentsModule,
38+
DataSourceModule,
39+
UsersModule,
3240
// Alternatively, we can use the `forRootAsync` method to register the module with dynamic options
3341
// HttpClientModule.forRootAsync({
3442
// useFactory: () => ({ baseUrl: 'https://nestjs.com' }),
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {
2+
ContextId,
3+
ContextIdFactory,
4+
ContextIdResolver,
5+
ContextIdResolverFn,
6+
ContextIdStrategy,
7+
HostComponentInfo,
8+
} from '@nestjs/core'
9+
import { Request } from 'express'
10+
11+
export class AggregateByTenantContextIdStrategy implements ContextIdStrategy {
12+
// A collection of context identifiers representing separate DI sub-trees per tenant
13+
private readonly tenants = new Map<string, ContextId>()
14+
15+
attach(
16+
contextId: ContextId,
17+
request: Request,
18+
): ContextIdResolverFn | ContextIdResolver {
19+
const tenantId = request.headers['x-tenant-id'] as string
20+
21+
if (!tenantId) {
22+
// OR log error depending on what we want to accomplish
23+
return () => contextId
24+
}
25+
26+
let tenantSubTreeId: ContextId
27+
28+
if (this.tenants.has(tenantId)) {
29+
tenantSubTreeId = this.tenants.get(tenantId)!
30+
} else {
31+
// Construct a new context id
32+
tenantSubTreeId = ContextIdFactory.create()
33+
this.tenants.set(tenantId, tenantSubTreeId)
34+
35+
// we can remove the tenant context id after a certain period of time
36+
// if not UserService instance is created once per tenant
37+
setTimeout(() => this.tenants.delete(tenantId), 3000)
38+
}
39+
40+
return {
41+
payload: { tenantId },
42+
resolve: (info: HostComponentInfo) =>
43+
info.isTreeDurable ? tenantSubTreeId : contextId,
44+
}
45+
}
46+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Module } from '@nestjs/common'
2+
import { DataSourceService } from './data-source.service'
3+
4+
@Module({
5+
providers: [
6+
DataSourceService,
7+
// {
8+
// provide: 'DATA_SOURCE',
9+
// useFactory: (payload) => new DataSource(...),
10+
// scope: Scope.REQUEST,
11+
// durable: true,
12+
// },
13+
],
14+
exports: [DataSourceService],
15+
})
16+
export class DataSourceModule {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { DataSourceService } from './data-source.service';
3+
4+
describe('DataSourceService', () => {
5+
let service: DataSourceService;
6+
7+
beforeEach(async () => {
8+
const module: TestingModule = await Test.createTestingModule({
9+
providers: [DataSourceService],
10+
}).compile();
11+
12+
service = module.get<DataSourceService>(DataSourceService);
13+
});
14+
15+
it('should be defined', () => {
16+
expect(service).toBeDefined();
17+
});
18+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Inject, Injectable, Scope } from '@nestjs/common'
2+
import { REQUEST } from '@nestjs/core'
3+
4+
@Injectable({ scope: Scope.REQUEST, durable: true })
5+
export class DataSourceService {
6+
constructor(@Inject(REQUEST) private readonly requestContext: unknown) {}
7+
}

src/users/dto/create-user.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export class CreateUserDto {}

src/users/dto/update-user.dto.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { PartialType } from '@nestjs/mapped-types';
2+
import { CreateUserDto } from './create-user.dto';
3+
4+
export class UpdateUserDto extends PartialType(CreateUserDto) {}

src/users/entities/user.entity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export class User {}

src/users/users.controller.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { UsersController } from './users.controller';
3+
import { UsersService } from './users.service';
4+
5+
describe('UsersController', () => {
6+
let controller: UsersController;
7+
8+
beforeEach(async () => {
9+
const module: TestingModule = await Test.createTestingModule({
10+
controllers: [UsersController],
11+
providers: [UsersService],
12+
}).compile();
13+
14+
controller = module.get<UsersController>(UsersController);
15+
});
16+
17+
it('should be defined', () => {
18+
expect(controller).toBeDefined();
19+
});
20+
});

src/users/users.controller.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
2+
import { UsersService } from './users.service';
3+
import { CreateUserDto } from './dto/create-user.dto';
4+
import { UpdateUserDto } from './dto/update-user.dto';
5+
6+
@Controller('users')
7+
export class UsersController {
8+
constructor(private readonly usersService: UsersService) {}
9+
10+
@Post()
11+
create(@Body() createUserDto: CreateUserDto) {
12+
return this.usersService.create(createUserDto);
13+
}
14+
15+
@Get()
16+
findAll() {
17+
return this.usersService.findAll();
18+
}
19+
20+
@Get(':id')
21+
findOne(@Param('id') id: string) {
22+
return this.usersService.findOne(+id);
23+
}
24+
25+
@Patch(':id')
26+
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
27+
return this.usersService.update(+id, updateUserDto);
28+
}
29+
30+
@Delete(':id')
31+
remove(@Param('id') id: string) {
32+
return this.usersService.remove(+id);
33+
}
34+
}

0 commit comments

Comments
 (0)