feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,2 @@
export const SystemKnexConnection ='SystemKnexConnection';
export const SystemKnexConnectionConfigure = 'SystemKnexConnectionConfigure';

View File

@@ -0,0 +1,12 @@
import { Controller, Get, Post } from '@nestjs/common';
@Controller('/system_db')
export class SystemDatabaseController {
constructor() {}
@Post()
@Get()
ping(){
}
}

View File

@@ -0,0 +1,49 @@
import Knex from 'knex';
import { Global, Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
SystemKnexConnection,
SystemKnexConnectionConfigure,
} from './SystemDB.constants';
import { SystemDatabaseController } from './SystemDB.controller';
import { knexSnakeCaseMappers } from 'objection';
const providers = [
{
provide: SystemKnexConnectionConfigure,
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
client: configService.get('systemDatabase.client'),
connection: {
host: configService.get('systemDatabase.host'),
user: configService.get('systemDatabase.user'),
password: configService.get('systemDatabase.password'),
database: configService.get('systemDatabase.databaseName'),
charset: 'utf8',
},
migrations: {
directory: configService.get('systemDatabase.migrationDir'),
},
seeds: {
directory: configService.get('systemDatabase.seedsDir'),
},
pool: { min: 0, max: 7 },
...knexSnakeCaseMappers({ upperCase: true }),
}),
},
{
provide: SystemKnexConnection,
inject: [SystemKnexConnectionConfigure],
useFactory: (knexConfig) => {
return Knex(knexConfig);
},
},
];
@Global()
@Module({
providers: [...providers],
exports: [...providers],
controllers: [SystemDatabaseController],
})
export class SystemDatabaseModule {}

View File

@@ -0,0 +1 @@
export const SystemModelsConnection = 'SystemModelsConnection';

View File

@@ -0,0 +1,42 @@
import { Knex } from 'knex';
import { Model } from 'objection';
import { Global, Module } from '@nestjs/common';
import { PlanSubscription } from '@/modules/Subscription/models/PlanSubscription';
import { TenantModel } from '@/modules/System/models/TenantModel';
import { SystemKnexConnection } from '../SystemDB/SystemDB.constants';
import { SystemModelsConnection } from './SystemModels.constants';
import { SystemUser } from '../models/SystemUser';
import { TenantMetadata } from '../models/TenantMetadataModel';
import { TenantRepository } from '../repositories/Tenant.repository';
const models = [SystemUser, PlanSubscription, TenantModel, TenantMetadata];
const modelProviders = models.map((model) => {
return {
provide: model.name,
useValue: model,
};
});
export const InjectSystemModel = (model: typeof Model) => ({
useValue: model,
provide: model.name,
});
const providers = [
...modelProviders,
{
provide: SystemModelsConnection,
inject: [SystemKnexConnection],
useFactory: async (systemKnex: Knex) => {
Model.knex(systemKnex);
},
},
];
@Global()
@Module({
providers: [...providers, TenantRepository],
exports: [...providers, TenantRepository],
})
export class SystemModelsModule {}

View File

@@ -0,0 +1,3 @@
import { BaseModel } from '@/models/Model';
export class SystemModel extends BaseModel {}

View File

@@ -0,0 +1,44 @@
import * as bcrypt from 'bcrypt';
import { BaseModel } from '@/models/Model';
export class SystemUser extends BaseModel {
public readonly firstName: string;
public readonly lastName: string;
public readonly email: string;
public password: string;
public readonly active: boolean;
public readonly tenantId: number;
public readonly verifyToken: string;
public readonly verified: boolean;
public readonly inviteAcceptedAt!: string;
static get tableName() {
return 'users';
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the invite accepted users.
*/
inviteAccepted(query) {
query.whereNotNull('invite_accepted_at');
},
};
}
async hashPassword(): Promise<void> {
const salt = await bcrypt.genSalt();
if (!/^\$2[abxy]?\$\d+\$/.test(this.password)) {
this.password = await bcrypt.hash(this.password, salt);
}
}
async checkPassword(plainPassword: string): Promise<boolean> {
return await bcrypt.compare(plainPassword, this.password);
}
}

View File

@@ -0,0 +1,17 @@
import * as R from 'ramda';
import { BaseModel } from '@/models/Model';
import { CustomViewBaseModelMixin } from '@/modules/CustomViews/CustomViewBaseModel';
import { MetadataModelMixin } from '@/modules/DynamicListing/models/MetadataModel';
import { SearchableBaseModelMixin } from '@/modules/DynamicListing/models/SearchableBaseModel';
import { ResourceableModelMixin } from '@/modules/Resource/models/ResourcableModel';
const ExtendedItem = R.pipe(
CustomViewBaseModelMixin,
SearchableBaseModelMixin,
ResourceableModelMixin,
MetadataModelMixin,
)(BaseModel);
export class TenantBaseModel extends ExtendedItem {}
export type TenantModelProxy<T> = () => T;

View File

@@ -0,0 +1,86 @@
import { BaseModel } from '@/models/Model';
import {
defaultOrganizationAddressFormat,
organizationAddressTextFormat,
} from '@/utils/address-text-format';
import { findByIsoCountryCode } from '@bigcapital/utils';
// import { getUploadedObjectUri } from '../../services/Attachments/utils';
export class TenantMetadata extends BaseModel {
public baseCurrency!: string;
public name!: string;
public tenantId!: number;
public industry!: string;
public location!: string;
public language!: string;
public timezone!: string;
public dateFormat!: string;
public fiscalYear!: string;
public primaryColor!: string;
public logoKey!: string;
public logoUri!: string;
public address!: Record<string, any>;
/**
* Json schema.
*/
static get jsonSchema() {
return {
type: 'object',
required: ['tenantId', 'name', 'baseCurrency'],
properties: {
tenantId: { type: 'integer' },
name: { type: 'string', maxLength: 255 },
industry: { type: 'string', maxLength: 255 },
location: { type: 'string', maxLength: 255 },
baseCurrency: { type: 'string', maxLength: 3 },
language: { type: 'string', maxLength: 255 },
timezone: { type: 'string', maxLength: 255 },
dateFormat: { type: 'string', maxLength: 255 },
fiscalYear: { type: 'string', maxLength: 255 },
primaryColor: { type: 'string', maxLength: 7 }, // Assuming hex color code
logoKey: { type: 'string', maxLength: 255 },
address: { type: 'object' },
},
};
}
/**
* Table name.
*/
static tableName = 'tenants_metadata';
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['logoUri'];
}
/**
* Organization logo url.
* @returns {string | null}
*/
// public get logoUri() {
// return this.logoKey ? getUploadedObjectUri(this.logoKey) : null;
// }
// /**
// * Retrieves the organization address formatted text.
// * @returns {string}
// */
public get addressTextFormatted() {
const addressCountry = findByIsoCountryCode(this.location);
return organizationAddressTextFormat(defaultOrganizationAddressFormat, {
organizationName: this.name,
address1: this.address?.address1,
address2: this.address?.address2,
state: this.address?.stateProvince,
city: this.address?.city,
postalCode: this.address?.postalCode,
phone: this.address?.phone,
country: addressCountry?.name ?? '',
});
}
}

View File

@@ -0,0 +1,83 @@
import { BaseModel } from '@/models/Model';
import { Model } from 'objection';
import { TenantMetadata } from './TenantMetadataModel';
import { PlanSubscription } from '@/modules/Subscription/models/PlanSubscription';
export class TenantModel extends BaseModel {
public readonly organizationId: string;
public readonly initializedAt: string;
public readonly seededAt: string;
public readonly builtAt: string;
public readonly metadata: TenantMetadata;
public readonly buildJobId: string;
public readonly upgradeJobId: string;
public readonly databaseBatch: string;
public readonly subscriptions: Array<PlanSubscription>;
/**
* Table name.
*/
static tableName = 'tenants';
/**
* Virtual attributes.
* @returns {string[]}
*/
static get virtualAttributes() {
return ['isReady', 'isBuildRunning', 'isUpgradeRunning'];
}
/**
* Tenant is ready.
* @returns {boolean}
*/
get isReady() {
return !!(this.initializedAt && this.seededAt);
}
/**
* Determines the tenant whether is build currently running.
* @returns {boolean}
*/
get isBuildRunning() {
return !!this.buildJobId;
}
/**
* Determines the tenant whether is upgrade currently running.
* @returns {boolean}
*/
get isUpgradeRunning() {
return !!this.upgradeJobId;
}
/**
* Relations mappings.
*/
static get relationMappings() {
const {
PlanSubscription,
} = require('../../Subscription/models/PlanSubscription');
const { TenantMetadata } = require('./TenantMetadataModel');
return {
metadata: {
relation: Model.HasOneRelation,
modelClass: TenantMetadata,
join: {
from: 'tenants.id',
to: 'tenants_metadata.tenantId',
},
},
subscriptions: {
relation: Model.HasManyRelation,
modelClass: PlanSubscription,
join: {
from: 'tenants.id',
to: 'subscription_plan_subscriptions.tenantId',
},
},
};
}
}

View File

@@ -0,0 +1,133 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import * as uniqid from 'uniqid';
import * as moment from 'moment';
import { TenantRepository as TenantBaseRepository } from '@/common/repository/TenantRepository';
import { SystemKnexConnection } from '../SystemDB/SystemDB.constants';
import { TenantModel } from '../models/TenantModel';
import { ConfigService } from '@nestjs/config';
import { TenantMetadata } from '../models/TenantMetadataModel';
@Injectable()
export class TenantRepository extends TenantBaseRepository {
constructor(
private readonly configService: ConfigService,
@Inject(SystemKnexConnection)
private readonly tenantDBKnex: Knex,
@Inject(TenantMetadata.name)
private readonly tenantMetadataModel: typeof TenantMetadata,
) {
super();
}
/**
* Gets the repository's model.
*/
get model(): typeof TenantModel {
return TenantModel.bindKnex(this.tenantDBKnex);
}
/**
* Creates a new tenant with random organization id.
*/
createWithUniqueOrgId(uniqId?: string) {
const organizationId = uniqid() || uniqId;
return this.model.query().insert({ organizationId });
}
/**
* Mark as seeded.
* @param {number} tenantId
*/
markAsSeeded() {
const seededAt = moment().toMySqlDateTime();
return this.model.query().update({ seededAt });
}
/**
* Mark the the given organization as initialized.
* @param {string} organizationId
*/
markAsInitialized() {
const initializedAt = moment().toMySqlDateTime();
return this.model.query().update({ initializedAt });
}
/**
* Marks the given tenant as built.
*/
markAsBuilt() {
const builtAt = moment().toMySqlDateTime();
return this.model.query().update({ builtAt });
}
/**
* Marks the given tenant as built.
* @param {string} buildJobId - The build job id.
*/
markAsBuilding(buildJobId: string) {
return this.model.query().update({ buildJobId });
}
/**
* Marks the given tenant as built.
*/
markAsBuildCompleted() {
return this.model.query().update({ buildJobId: null });
}
/**
* Marks the given tenant as upgrading.
* @param {number} tenantId
* @param {string} upgradeJobId
* @returns
*/
markAsUpgrading(tenantId, upgradeJobId) {
return this.model.query().update({ upgradeJobId }).where({ id: tenantId });
}
/**
* Marks the given tenant as upgraded.
* @param {number} tenantId
* @returns
*/
markAsUpgraded(tenantId) {
return this.model
.query()
.update({ upgradeJobId: null })
.where({ id: tenantId });
}
/**
* Saves the metadata of the given tenant.
* @param {number} tenantId - The tenant id.
* @param {Record<string, any>} metadata - The metadata to save.
*/
async saveMetadata(tenantId: number, metadata: Record<string, any>) {
const foundMetadata = await this.tenantMetadataModel
.query()
.findOne({ tenantId });
const updateOrInsert = foundMetadata ? 'patch' : 'insert';
return this.tenantMetadataModel
.query()
[updateOrInsert]({
tenantId,
...metadata,
})
.where({ tenantId });
}
/**
* Adds organization database latest batch number.
* @param {number} tenantId
* @param {number} version
*/
flagTenantDBBatch() {
return this.model.query().update({
databaseBatch: this.configService.get('databaseBatch'),
});
}
}