mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 05:40:31 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
SetMetadata,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { TenancyContext } from './TenancyContext.service';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
|
||||
|
||||
export const IS_IGNORE_TENANT_INITIALIZED = 'IS_IGNORE_TENANT_INITIALIZED';
|
||||
export const IgnoreTenantInitializedRoute = () => SetMetadata(IS_IGNORE_TENANT_INITIALIZED, true);
|
||||
|
||||
@Injectable()
|
||||
export class EnsureTenantIsInitializedGuard implements CanActivate {
|
||||
constructor(private readonly tenancyContext: TenancyContext, private reflector: Reflector) {}
|
||||
|
||||
/**
|
||||
* Validate the tenant of the current request is initialized..
|
||||
* @param {ExecutionContext} context
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isIgnoreEnsureTenantInitialized = this.reflector.getAllAndOverride<boolean>(
|
||||
IS_IGNORE_TENANT_INITIALIZED,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(
|
||||
IS_PUBLIC_ROUTE,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
// Skip the guard early if the route marked as public or ignored.
|
||||
if (isPublic || isIgnoreEnsureTenantInitialized) {
|
||||
return true;
|
||||
}
|
||||
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,47 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Inject,
|
||||
Injectable,
|
||||
SetMetadata,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { TenancyContext } from './TenancyContext.service';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
|
||||
|
||||
export const IS_IGNORE_TENANT_SEEDED = 'IS_IGNORE_TENANT_SEEDED';
|
||||
export const IgnoreTenantSeededRoute = () => SetMetadata(IS_IGNORE_TENANT_SEEDED, true);
|
||||
|
||||
@Injectable()
|
||||
export class EnsureTenantIsSeededGuard implements CanActivate {
|
||||
constructor(private readonly tenancyContext: TenancyContext, private reflector: Reflector) {}
|
||||
|
||||
/**
|
||||
* Validate the tenant of the current request is seeded.
|
||||
* @param {ExecutionContext} context
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(
|
||||
IS_PUBLIC_ROUTE,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
const isIgnoreEnsureTenantSeeded = this.reflector.getAllAndOverride<boolean>(
|
||||
IS_IGNORE_TENANT_SEEDED,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
if (isPublic || isIgnoreEnsureTenantSeeded) {
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
29
packages/server/src/modules/Tenancy/Tenancy.module.ts
Normal file
29
packages/server/src/modules/Tenancy/Tenancy.module.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { EnsureTenantIsInitializedGuard } from "./EnsureTenantIsInitialized.guard";
|
||||
import { TenancyGlobalGuard } from "./TenancyGlobal.guard";
|
||||
import { EnsureTenantIsSeededGuard } from "./EnsureTenantIsSeeded.guards";
|
||||
import { APP_GUARD } from "@nestjs/core";
|
||||
import { TenancyContext } from "./TenancyContext.service";
|
||||
import { TenantController } from "./Tenant.controller";
|
||||
|
||||
|
||||
@Module({
|
||||
exports: [TenancyContext],
|
||||
controllers: [TenantController],
|
||||
providers: [
|
||||
TenancyContext,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: TenancyGlobalGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: EnsureTenantIsInitializedGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: EnsureTenantIsSeededGuard
|
||||
}
|
||||
]
|
||||
})
|
||||
export class TenancyModule {}
|
||||
@@ -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,54 @@
|
||||
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.
|
||||
* @param {boolean} withMetadata - If true, the tenant metadata will be fetched.
|
||||
* @returns
|
||||
*/
|
||||
getTenant(withMetadata: boolean = false) {
|
||||
// Get the tenant from the request headers.
|
||||
const organizationId = this.cls.get('organizationId');
|
||||
|
||||
if (!organizationId) {
|
||||
throw new Error('Tenant not found');
|
||||
}
|
||||
const query = this.systemTenantModel.query().findOne({ organizationId });
|
||||
|
||||
if (withMetadata) {
|
||||
query.withGraphFetched('metadata');
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
async getTenantMetadata() {
|
||||
const tenant = await this.getTenant(true);
|
||||
|
||||
return tenant?.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current system user.
|
||||
* @returns {Promise<SystemUser>}
|
||||
*/
|
||||
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,56 @@
|
||||
import knex from 'knex';
|
||||
import * as LRUCache from 'lru-cache';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
import { ClsModule, ClsService } from 'nestjs-cls';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TENANCY_DB_CONNECTION } from './TenancyDB.constants';
|
||||
import { UnitOfWork } from './UnitOfWork.service';
|
||||
|
||||
const lruCache = new LRUCache();
|
||||
|
||||
export const TenancyDatabaseProxyProvider = ClsModule.forFeatureAsync({
|
||||
provide: TENANCY_DB_CONNECTION,
|
||||
global: true,
|
||||
strict: true,
|
||||
inject: [ConfigService, ClsService],
|
||||
useFactory: async (configService: ConfigService, cls: ClsService) => () => {
|
||||
const organizationId = cls.get('organizationId');
|
||||
const database = `bigcapital_tenant_${organizationId}`;
|
||||
const cachedInstance = lruCache.get(database);
|
||||
|
||||
if (cachedInstance) {
|
||||
return cachedInstance;
|
||||
}
|
||||
const knexInstance = knex({
|
||||
client: configService.get('tenantDatabase.client'),
|
||||
connection: {
|
||||
host: configService.get('tenantDatabase.host'),
|
||||
user: configService.get('tenantDatabase.user'),
|
||||
password: configService.get('tenantDatabase.password'),
|
||||
database,
|
||||
charset: 'utf8',
|
||||
},
|
||||
migrations: {
|
||||
directory: configService.get('tenantDatabase.migrationsDir'),
|
||||
},
|
||||
seeds: {
|
||||
directory: configService.get('tenantDatabase.seedsDir'),
|
||||
},
|
||||
pool: { min: 0, max: 7 },
|
||||
...knexSnakeCaseMappers({ upperCase: true }),
|
||||
});
|
||||
lruCache.set(database, knexInstance);
|
||||
|
||||
return knexInstance;
|
||||
},
|
||||
type: 'function',
|
||||
});
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TenancyDatabaseProxyProvider],
|
||||
providers: [UnitOfWork],
|
||||
exports: [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;
|
||||
}
|
||||
};
|
||||
}
|
||||
44
packages/server/src/modules/Tenancy/TenancyGlobal.guard.ts
Normal file
44
packages/server/src/modules/Tenancy/TenancyGlobal.guard.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
SetMetadata,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
|
||||
|
||||
export const IS_TENANT_AGNOSTIC = 'IS_TENANT_AGNOSTIC';
|
||||
|
||||
export const TenantAgnosticRoute = () => SetMetadata(IS_TENANT_AGNOSTIC, true);
|
||||
|
||||
@Injectable()
|
||||
export class TenancyGlobalGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
/**
|
||||
* Validates the organization ID in the request headers.
|
||||
* @param {ExecutionContext} context
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const organizationId = request.headers['organization-id'];
|
||||
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(
|
||||
IS_PUBLIC_ROUTE,
|
||||
[context.getHandler(), context.getClass()],
|
||||
)
|
||||
const isTenantAgnostic = this.reflector.getAllAndOverride<boolean>(
|
||||
IS_TENANT_AGNOSTIC,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
if (isPublic || isTenantAgnostic) {
|
||||
return true;
|
||||
}
|
||||
if (!organizationId) {
|
||||
throw new UnauthorizedException('Organization ID is required.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const TenancyModelsConnection = 'TenancyModelsConnection';
|
||||
@@ -0,0 +1,110 @@
|
||||
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.model';
|
||||
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
|
||||
import { Expense } from '@/modules/Expenses/models/Expense.model';
|
||||
import { ExpenseCategory } from '@/modules/Expenses/models/ExpenseCategory.model';
|
||||
import { ItemCategory } from '@/modules/ItemCategories/models/ItemCategory.model';
|
||||
import { TaxRateModel } from '@/modules/TaxRates/models/TaxRate.model';
|
||||
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
|
||||
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
|
||||
import { ItemWarehouseQuantity } from '@/modules/Warehouses/models/ItemWarehouseQuantity';
|
||||
import { Branch } from '@/modules/Branches/models/Branch.model';
|
||||
import { SaleEstimate } from '@/modules/SaleEstimates/models/SaleEstimate';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { Contact } from '@/modules/Contacts/models/Contact';
|
||||
import { Document } from '@/modules/ChromiumlyTenancy/models/Document';
|
||||
import { DocumentLink } from '@/modules/ChromiumlyTenancy/models/DocumentLink';
|
||||
import { Vendor } from '@/modules/Vendors/models/Vendor';
|
||||
import { Bill } from '@/modules/Bills/models/Bill';
|
||||
import { BillPayment } from '@/modules/BillPayments/models/BillPayment';
|
||||
import { BillPaymentEntry } from '@/modules/BillPayments/models/BillPaymentEntry';
|
||||
import { BillLandedCostEntry } from '@/modules/BillLandedCosts/models/BillLandedCostEntry';
|
||||
import { BillLandedCost } from '@/modules/BillLandedCosts/models/BillLandedCost';
|
||||
import { VendorCreditAppliedBill } from '@/modules/VendorCreditsApplyBills/models/VendorCreditAppliedBill';
|
||||
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
||||
import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry';
|
||||
import { CreditNoteAppliedInvoice } from '@/modules/CreditNotesApplyInvoice/models/CreditNoteAppliedInvoice';
|
||||
import { CreditNote } from '@/modules/CreditNotes/models/CreditNote';
|
||||
import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink';
|
||||
import { SaleReceipt } from '@/modules/SaleReceipts/models/SaleReceipt';
|
||||
import { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal';
|
||||
import { ManualJournalEntry } from '@/modules/ManualJournals/models/ManualJournalEntry';
|
||||
import { RefundCreditNote } from '@/modules/CreditNoteRefunds/models/RefundCreditNote';
|
||||
import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit';
|
||||
import { RefundVendorCredit } from '@/modules/VendorCreditsRefund/models/RefundVendorCredit';
|
||||
import { PaymentReceived } from '@/modules/PaymentReceived/models/PaymentReceived';
|
||||
import { Model } from 'objection';
|
||||
import { ClsModule } from 'nestjs-cls';
|
||||
import { TenantUser } from './models/TenantUser.model';
|
||||
|
||||
const models = [
|
||||
Item,
|
||||
Account,
|
||||
ItemEntry,
|
||||
AccountTransaction,
|
||||
Expense,
|
||||
ExpenseCategory,
|
||||
ItemCategory,
|
||||
TaxRateModel,
|
||||
PdfTemplateModel,
|
||||
Warehouse,
|
||||
ItemWarehouseQuantity,
|
||||
Branch,
|
||||
SaleEstimate,
|
||||
Customer,
|
||||
Contact,
|
||||
Document,
|
||||
DocumentLink,
|
||||
Vendor,
|
||||
Bill,
|
||||
BillPayment,
|
||||
BillPaymentEntry,
|
||||
BillLandedCost,
|
||||
BillLandedCostEntry,
|
||||
VendorCreditAppliedBill,
|
||||
SaleInvoice,
|
||||
CreditNoteAppliedInvoice,
|
||||
CreditNote,
|
||||
RefundCreditNote,
|
||||
PaymentLink,
|
||||
SaleReceipt,
|
||||
ManualJournal,
|
||||
ManualJournalEntry,
|
||||
VendorCredit,
|
||||
VendorCreditAppliedBill,
|
||||
RefundVendorCredit,
|
||||
PaymentReceived,
|
||||
PaymentReceivedEntry,
|
||||
TenantUser,
|
||||
];
|
||||
|
||||
/**
|
||||
* Decorator factory that registers a model with the tenancy system.
|
||||
* @param model The model class to register
|
||||
*/
|
||||
export function RegisterTenancyModel(model: typeof Model) {
|
||||
return ClsModule.forFeatureAsync({
|
||||
provide: model.name,
|
||||
inject: [TENANCY_DB_CONNECTION],
|
||||
global: true,
|
||||
useFactory: (tenantKnex: () => Knex) => () => {
|
||||
return model.bindKnex(tenantKnex());
|
||||
},
|
||||
strict: true,
|
||||
type: 'function',
|
||||
});
|
||||
}
|
||||
|
||||
// Register all models using the decorator
|
||||
const modelProviders = models.map((model) => RegisterTenancyModel(model));
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [...modelProviders],
|
||||
exports: [...modelProviders],
|
||||
})
|
||||
export class TenancyModelsModule {}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Model } from 'objection';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Role } from '../../../Roles/models/Role.model';
|
||||
|
||||
export class TenantUser extends TenantBaseModel {
|
||||
firstName!: string;
|
||||
lastName!: string;
|
||||
inviteAcceptedAt!: Date;
|
||||
roleId!: number;
|
||||
|
||||
role!: Role;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'users';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['isInviteAccepted', 'fullName'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the user ivnite is accept.
|
||||
*/
|
||||
get isInviteAccepted() {
|
||||
return !!this.inviteAcceptedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full name attribute.
|
||||
*/
|
||||
get fullName() {
|
||||
return `${this.firstName} ${this.lastName}`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Role = require('models/Role');
|
||||
|
||||
return {
|
||||
/**
|
||||
* User belongs to user.
|
||||
*/
|
||||
role: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Role.default,
|
||||
join: {
|
||||
from: 'users.roleId',
|
||||
to: 'roles.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
4
packages/server/src/modules/Tenancy/Tenant.controller.ts
Normal file
4
packages/server/src/modules/Tenancy/Tenant.controller.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Controller } from "@nestjs/common";
|
||||
|
||||
@Controller('/')
|
||||
export class TenantController {}
|
||||
Reference in New Issue
Block a user