Skip to content

Commit 65513d8

Browse files
sru-thybharathkeyvalue
authored andcommitted
FEAT: use new database connection per tenant
1 parent 67f6134 commit 65513d8

File tree

7 files changed

+146
-17
lines changed

7 files changed

+146
-17
lines changed

src/authorization/entity/abstract.tenant.entity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import BaseEntity from './base.entity';
44
class AbstractTenantEntity extends BaseEntity {
55
@Column({
66
type: 'uuid',
7-
default: () => "current_setting('app.current_tenant')",
7+
default: () => "current_setting('app.tenant_id')::uuid",
88
})
99
public tenantId!: string;
1010
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,56 @@
11
import { DataSource, EntityTarget, ObjectLiteral, Repository } from 'typeorm';
2+
import { getConnection } from '../../util/database.connection';
23

34
export class BaseRepository<T extends ObjectLiteral> extends Repository<T> {
5+
private entityClass: any;
6+
47
constructor(entity: EntityTarget<T>, dataSource: DataSource) {
58
super(entity, dataSource.createEntityManager());
9+
this.entityClass = entity;
10+
}
11+
12+
protected async getDynamicRepository(): Promise<Repository<T>> {
13+
const connection = await getConnection();
14+
return connection.getRepository(this.entityClass.name);
15+
}
16+
17+
async find(options?: any): Promise<T[]> {
18+
const repository = await this.getDynamicRepository();
19+
return repository.find(options);
20+
}
21+
22+
async findOne(options: any): Promise<T | null> {
23+
const repository = await this.getDynamicRepository();
24+
return repository.findOne(options);
25+
}
26+
27+
async findOneOrFail(options: any): Promise<T> {
28+
const repository = await this.getDynamicRepository();
29+
return repository.findOneOrFail(options);
30+
}
31+
32+
async findOneBy(options: any): Promise<T | null> {
33+
const repository = await this.getDynamicRepository();
34+
return repository.findOneBy(options);
35+
}
36+
37+
async update(criteria: any, partialEntity: any): Promise<any> {
38+
const repository = await this.getDynamicRepository();
39+
return repository.update(criteria, partialEntity);
40+
}
41+
42+
async delete(criteria: any): Promise<any> {
43+
const repository = await this.getDynamicRepository();
44+
return repository.delete(criteria);
45+
}
46+
47+
async softDelete(criteria: any): Promise<any> {
48+
const repository = await this.getDynamicRepository();
49+
return repository.softDelete(criteria);
50+
}
51+
52+
async count(options?: any): Promise<number> {
53+
const repository = await this.getDynamicRepository();
54+
return repository.count(options);
655
}
756
}

src/authorization/service/entity.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { EntityPermissionRepository } from '../repository/entityPermission.repos
1515
import { PermissionRepository } from '../repository/permission.repository';
1616
import { EntityServiceInterface } from './entity.service.interface';
1717
import { ExecutionManager } from '../../util/execution.manager';
18+
import { getConnection } from '../../util/database.connection';
1819

1920
@Injectable()
2021
export class EntityService implements EntityServiceInterface {
@@ -61,8 +62,8 @@ export class EntityService implements EntityServiceInterface {
6162
if (!existingEntity) {
6263
throw new EntityNotFoundException(id);
6364
}
64-
65-
await this.dataSource.manager.transaction(async (entityManager) => {
65+
const entityManager = (await getConnection()).manager;
66+
await entityManager.transaction(async (entityManager) => {
6667
const entityRepo = entityManager.getRepository(EntityModel);
6768
const entityPermissionRepo = entityManager.getRepository(
6869
EntityPermission,

src/database/database.module.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
22
import { TypeOrmModule } from '@nestjs/typeorm';
33
import { ConfigModule, ConfigService } from '@nestjs/config';
44
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
5-
import { ExecutionManager } from '../util/execution.manager';
65

76
@Module({
87
imports: [
@@ -23,18 +22,6 @@ import { ExecutionManager } from '../util/execution.manager';
2322
namingStrategy: new SnakeNamingStrategy(),
2423
synchronize: false,
2524
logging: true,
26-
extra: {
27-
poolMiddleware: async (query: string, params: any[]) => {
28-
const tenantId = ExecutionManager.getTenantId();
29-
if (tenantId) {
30-
return [
31-
`SELECT set_config('app.tenant_id', ${tenantId}, false) ${query}`,
32-
params,
33-
];
34-
}
35-
return [query, params];
36-
},
37-
},
3825
}),
3926
}),
4027
],

src/middleware/executionId.middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class ExecutionContextBinder implements NestMiddleware {
1616
}
1717
next();
1818
} catch (error) {
19-
next(error);
19+
next();
2020
}
2121
});
2222
}

src/migrations/1733833844028-MultiTenantFeature.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,33 @@ export class MultiTenantFeature1733833844028 implements MigrationInterface {
104104
CREATE POLICY tenant_isolation_policy ON "entity_permission"
105105
USING (tenant_id = current_setting('app.tenant_id')::uuid);
106106
`);
107+
await queryRunner.query(`
108+
ALTER TABLE "entity_permission" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid;
109+
ALTER TABLE "group" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid;
110+
ALTER TABLE "group_permission" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid;
111+
ALTER TABLE "entity_model" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid;
112+
ALTER TABLE "role" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid;
113+
ALTER TABLE "group_role" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid;
114+
ALTER TABLE "role_permission" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid;
115+
ALTER TABLE "user" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid;
116+
ALTER TABLE "user_group" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid;
117+
ALTER TABLE "user_permission" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid;
118+
`);
107119
}
108120

109121
public async down(queryRunner: QueryRunner): Promise<void> {
122+
await queryRunner.query(`
123+
ALTER TABLE "user_permission" ALTER COLUMN "tenant_id" DROP DEFAULT;
124+
ALTER TABLE "user_group" ALTER COLUMN "tenant_id" DROP DEFAULT;
125+
ALTER TABLE "user" ALTER COLUMN "tenant_id" DROP DEFAULT;
126+
ALTER TABLE "role_permission" ALTER COLUMN "tenant_id" DROP DEFAULT;
127+
ALTER TABLE "group_role" ALTER COLUMN "tenant_id" DROP DEFAULT;
128+
ALTER TABLE "role" ALTER COLUMN "tenant_id" DROP DEFAULT;
129+
ALTER TABLE "entity_model" ALTER COLUMN "tenant_id" DROP DEFAULT;
130+
ALTER TABLE "group_permission" ALTER COLUMN "tenant_id" DROP DEFAULT;
131+
ALTER TABLE "group" ALTER COLUMN "tenant_id" DROP DEFAULT;
132+
ALTER TABLE "entity_permission" ALTER COLUMN "tenant_id" DROP DEFAULT;
133+
`);
110134
await queryRunner.query(`
111135
DROP POLICY tenant_isolation_policy ON "user";
112136
DROP POLICY tenant_isolation_policy ON "role";

src/util/database.connection.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { getConnectionManager, DataSource } from 'typeorm';
2+
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
3+
import { LoggerService } from '../logger/logger.service';
4+
import { ExecutionManager } from './execution.manager';
5+
6+
/**
7+
* Get connection based on the logged in tenant
8+
* @returns connection
9+
*/
10+
11+
export async function getConnection(): Promise<DataSource> {
12+
const tenantName = ExecutionManager.getTenantId();
13+
return getConnectionForTenant(tenantName);
14+
}
15+
16+
export async function getConnectionForTenant(
17+
tenantId: string,
18+
): Promise<DataSource> {
19+
if (getConnectionManager().has(tenantId)) {
20+
const con = getConnectionManager().get(tenantId);
21+
const existingConnection = await Promise.resolve(
22+
con.isConnected ? con : con.connect(),
23+
);
24+
await switchToTenant(tenantId, existingConnection);
25+
26+
return existingConnection;
27+
}
28+
29+
const newConnection: DataSource = await new DataSource({
30+
name: tenantId,
31+
type: 'postgres',
32+
host: process.env.POSTGRES_HOST,
33+
port: Number(process.env.POSTGRES_PORT),
34+
username: process.env.POSTGRES_USER,
35+
password: process.env.POSTGRES_PASSWORD,
36+
database: process.env.POSTGRES_DB,
37+
entities: [
38+
__dirname + '/../**/*.entity.ts',
39+
__dirname + '/../**/*.entity.js',
40+
],
41+
synchronize: false,
42+
logging: ['error'],
43+
namingStrategy: new SnakeNamingStrategy(),
44+
extra: { max: process.env.POSTGRES_TENANT_MAX_CONNECTION_LIMIT },
45+
}).initialize();
46+
47+
await switchToTenant(tenantId, newConnection);
48+
49+
return newConnection;
50+
}
51+
52+
const switchToTenant = async (
53+
tenantId: string,
54+
connection: DataSource,
55+
): Promise<void> => {
56+
try {
57+
await connection.query(`select set_config('app.tenant_id', $1, false)`, [
58+
tenantId,
59+
]);
60+
} catch (error) {
61+
const logger = LoggerService.getInstance('bootstrap()');
62+
logger.error(
63+
`Failed to switch to tenant: ${tenantId}, error: ${JSON.stringify(
64+
error,
65+
)}]`,
66+
);
67+
}
68+
};

0 commit comments

Comments
 (0)