mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +00:00
feat: wip migrate server to nestjs
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { TenancyContext } from './TenancyContext.service';
|
||||
|
||||
@Injectable()
|
||||
export class EnsureTenantIsInitializedGuard implements CanActivate {
|
||||
constructor(private readonly tenancyContext: TenancyContext) {}
|
||||
|
||||
/**
|
||||
* Validate the tenant of the current request is initialized..
|
||||
* @param {ExecutionContext} context
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const tenant = await this.tenancyContext.getTenant();
|
||||
|
||||
if (!tenant?.initializedAt) {
|
||||
throw new UnauthorizedException({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Tenant database is not migrated with application schema yet.',
|
||||
errors: [{ type: 'TENANT.DATABASE.NOT.INITALIZED' }],
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Inject,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { TenancyContext } from './TenancyContext.service';
|
||||
|
||||
@Injectable()
|
||||
export class EnsureTenantIsSeededGuard implements CanActivate {
|
||||
constructor(private readonly tenancyContext: TenancyContext) {}
|
||||
|
||||
/**
|
||||
* Validate the tenant of the current request is seeded.
|
||||
* @param {ExecutionContext} context
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const tenant = await this.tenancyContext.getTenant();
|
||||
|
||||
if (!tenant.seededAt) {
|
||||
throw new UnauthorizedException({
|
||||
message: 'Tenant database is not seeded with initial data yet.',
|
||||
errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }],
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { RedisClientOptions } from 'redis';
|
||||
import { DynamicModule, Module } from '@nestjs/common';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
|
||||
interface TenancyCacheModuleConfig {
|
||||
tenantId: number;
|
||||
}
|
||||
|
||||
@Module({})
|
||||
export class TenancyCacheModule {
|
||||
static register(config: TenancyCacheModuleConfig): DynamicModule {
|
||||
return {
|
||||
module: TenancyCacheModule,
|
||||
imports: [CacheModule.register<RedisClientOptions>({})],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { SystemUser } from '../System/models/SystemUser';
|
||||
import { TenantModel } from '../System/models/TenantModel';
|
||||
|
||||
@Injectable()
|
||||
export class TenancyContext {
|
||||
constructor(
|
||||
private readonly cls: ClsService,
|
||||
|
||||
@Inject(SystemUser.name)
|
||||
private readonly systemUserModel: typeof SystemUser,
|
||||
|
||||
@Inject(TenantModel.name)
|
||||
private readonly systemTenantModel: typeof TenantModel,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the current tenant.
|
||||
* @returns
|
||||
*/
|
||||
getTenant() {
|
||||
// Get the tenant from the request headers.
|
||||
const organizationId = this.cls.get('organizationId');
|
||||
|
||||
return this.systemTenantModel.query().findOne({ organizationId });
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
getSystemUser() {
|
||||
// Get the user from the request headers.
|
||||
const userId = this.cls.get('userId');
|
||||
|
||||
return this.systemUserModel.query().findOne({ id: userId });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const TENANCY_DB_CONNECTION = 'TENANCY_DB_CONNECTION';
|
||||
@@ -0,0 +1,47 @@
|
||||
import knex from 'knex';
|
||||
import { Global, Module, Scope } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TENANCY_DB_CONNECTION } from './TenancyDB.constants';
|
||||
import { UnitOfWork } from './UnitOfWork.service';
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
|
||||
const connectionFactory = {
|
||||
provide: TENANCY_DB_CONNECTION,
|
||||
scope: Scope.REQUEST,
|
||||
useFactory: async (
|
||||
request: Request,
|
||||
configService: ConfigService,
|
||||
cls: ClsService,
|
||||
) => {
|
||||
const organizationId = cls.get('organizationId');
|
||||
|
||||
return knex({
|
||||
client: configService.get('tenantDatabase.client'),
|
||||
connection: {
|
||||
host: configService.get('tenantDatabase.host'),
|
||||
user: configService.get('tenantDatabase.user'),
|
||||
password: configService.get('tenantDatabase.password'),
|
||||
database: `bigcapital_tenant_${organizationId}`,
|
||||
charset: 'utf8',
|
||||
},
|
||||
migrations: {
|
||||
directory: configService.get('tenantDatabase.migrationDir'),
|
||||
},
|
||||
seeds: {
|
||||
directory: configService.get('tenantDatabase.seedsDir'),
|
||||
},
|
||||
pool: { min: 0, max: 7 },
|
||||
...knexSnakeCaseMappers({ upperCase: true }),
|
||||
});
|
||||
},
|
||||
inject: [REQUEST, ConfigService, ClsService],
|
||||
};
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [connectionFactory, UnitOfWork],
|
||||
exports: [TENANCY_DB_CONNECTION, UnitOfWork],
|
||||
})
|
||||
export class TenancyDatabaseModule {}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Enumeration that represents transaction isolation levels for use with the {@link Transactional} annotation
|
||||
*/
|
||||
export enum IsolationLevel {
|
||||
/**
|
||||
* A constant indicating that dirty reads, non-repeatable reads and phantom reads can occur.
|
||||
*/
|
||||
READ_UNCOMMITTED = 'read uncommitted',
|
||||
/**
|
||||
* A constant indicating that dirty reads are prevented; non-repeatable reads and phantom reads can occur.
|
||||
*/
|
||||
READ_COMMITTED = 'read committed',
|
||||
/**
|
||||
* A constant indicating that dirty reads and non-repeatable reads are prevented; phantom reads can occur.
|
||||
*/
|
||||
REPEATABLE_READ = 'repeatable read',
|
||||
/**
|
||||
* A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented.
|
||||
*/
|
||||
SERIALIZABLE = 'serializable',
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} maybeTrx
|
||||
* @returns {maybeTrx is import('objection').TransactionOrKnex & { executionPromise: Promise<any> }}
|
||||
*/
|
||||
function checkIsTransaction(maybeTrx) {
|
||||
return Boolean(maybeTrx && maybeTrx.executionPromise);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a transaction to be complete.
|
||||
* @param {import('objection').TransactionOrKnex} [trx]
|
||||
*/
|
||||
export async function waitForTransaction(trx) {
|
||||
return Promise.resolve(checkIsTransaction(trx) ? trx.executionPromise : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback when the transaction is done.
|
||||
* @param {import('objection').TransactionOrKnex | undefined} trx
|
||||
* @param {Function} callback
|
||||
*/
|
||||
export function runAfterTransaction(trx, callback) {
|
||||
waitForTransaction(trx).then(
|
||||
() => {
|
||||
// If transaction success, then run action
|
||||
return Promise.resolve(callback()).catch((error) => {
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
},
|
||||
() => {
|
||||
// Ignore transaction error
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Transaction } from 'objection';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsolationLevel } from './TransactionsHooks';
|
||||
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
|
||||
|
||||
@Injectable()
|
||||
export class UnitOfWork {
|
||||
constructor(
|
||||
@Inject(TENANCY_DB_CONNECTION)
|
||||
private readonly tenantKex: Knex,
|
||||
) {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {} work
|
||||
* @param {IsolationLevel} isolationLevel
|
||||
* @returns {}
|
||||
*/
|
||||
public withTransaction = async <T>(
|
||||
work: (knex: Knex.Transaction) => Promise<T> | T,
|
||||
trx?: Transaction,
|
||||
isolationLevel: IsolationLevel = IsolationLevel.READ_UNCOMMITTED,
|
||||
): Promise<T> => {
|
||||
const knex = this.tenantKex;
|
||||
let _trx = trx;
|
||||
|
||||
if (!_trx) {
|
||||
_trx = await knex.transaction({ isolationLevel });
|
||||
}
|
||||
try {
|
||||
const result = await work(_trx);
|
||||
|
||||
if (!trx) {
|
||||
_trx.commit();
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (!trx) {
|
||||
_trx.rollback();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ClsService, ClsServiceManager } from 'nestjs-cls';
|
||||
|
||||
export class TenancyGlobalMiddleware implements NestMiddleware {
|
||||
constructor(private readonly cls: ClsService) {}
|
||||
/**
|
||||
* Validates the organization ID in the request headers.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public use(req: Request, res: Response, next: NextFunction) {
|
||||
const organizationId = req.headers['organization-id'];
|
||||
|
||||
if (!organizationId) {
|
||||
throw new UnauthorizedException('Organization ID is required.');
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class TenancyIdClsInterceptor implements NestInterceptor {
|
||||
constructor(private readonly cls: ClsService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const organizationId = request.headers['organization-id'];
|
||||
// this.cls.get('organizationId');
|
||||
|
||||
// console.log(organizationId, 'organizationId22');
|
||||
|
||||
return next.handle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const TenancyModelsConnection = 'TenancyModelsConnection';
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Global, Module, Scope } from '@nestjs/common';
|
||||
import { TENANCY_DB_CONNECTION } from '../TenancyDB/TenancyDB.constants';
|
||||
import { Item } from '../../../modules/Items/models/Item';
|
||||
import { Account } from '@/modules/Accounts/models/Account';
|
||||
|
||||
const models = [Item, Account];
|
||||
const modelProviders = models.map((model) => {
|
||||
return {
|
||||
provide: model.name,
|
||||
inject: [TENANCY_DB_CONNECTION],
|
||||
scope: Scope.REQUEST,
|
||||
useFactory: async (tenantKnex: Knex) => {
|
||||
return model.bindKnex(tenantKnex);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [...modelProviders],
|
||||
exports: [...modelProviders],
|
||||
})
|
||||
export class TenancyModelsModule {}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { EnsureTenantIsSeededGuard } from '../Tenancy/EnsureTenantIsSeeded.guards';
|
||||
import { EnsureTenantIsInitializedGuard } from '../Tenancy/EnsureTenantIsInitialized.guard';
|
||||
|
||||
@UseGuards(EnsureTenantIsInitializedGuard)
|
||||
@UseGuards(EnsureTenantIsSeededGuard)
|
||||
export class TenantController {}
|
||||
Reference in New Issue
Block a user