mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export const SystemKnexConnection ='SystemKnexConnection';
|
||||
export const SystemKnexConnectionConfigure = 'SystemKnexConnectionConfigure';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Controller, Get, Post } from '@nestjs/common';
|
||||
|
||||
@Controller('/system_db')
|
||||
export class SystemDatabaseController {
|
||||
constructor() {}
|
||||
|
||||
@Post()
|
||||
@Get()
|
||||
ping(){
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -0,0 +1 @@
|
||||
export const SystemModelsConnection = 'SystemModelsConnection';
|
||||
@@ -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 {}
|
||||
3
packages/server/src/modules/System/models/SystemModel.ts
Normal file
3
packages/server/src/modules/System/models/SystemModel.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { BaseModel } from '@/models/Model';
|
||||
|
||||
export class SystemModel extends BaseModel {}
|
||||
44
packages/server/src/modules/System/models/SystemUser.ts
Normal file
44
packages/server/src/modules/System/models/SystemUser.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
17
packages/server/src/modules/System/models/TenantBaseModel.ts
Normal file
17
packages/server/src/modules/System/models/TenantBaseModel.ts
Normal 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;
|
||||
@@ -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 ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
83
packages/server/src/modules/System/models/TenantModel.ts
Normal file
83
packages/server/src/modules/System/models/TenantModel.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user