mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
refactor: implement tenant database management and seeding utilities
This commit is contained in:
@@ -25,6 +25,9 @@
|
||||
"@bigcapital/server": "*",
|
||||
"@bigcapital/utils": "*",
|
||||
"@liaoliaots/nestjs-redis": "^10.0.0",
|
||||
"@aws-sdk/client-s3": "^3.576.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.583.0",
|
||||
"@casl/ability": "^5.4.3",
|
||||
"@nestjs/bull": "^10.2.1",
|
||||
"@nestjs/bullmq": "^10.2.2",
|
||||
"@nestjs/cache-manager": "^2.2.2",
|
||||
@@ -63,6 +66,7 @@
|
||||
"knex": "^3.1.0",
|
||||
"lamda": "^0.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^6.0.0",
|
||||
"mathjs": "^9.4.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-range": "^4.0.2",
|
||||
|
||||
100
packages/server-nest/src/libs/migration-seed/FsMigrations.ts
Normal file
100
packages/server-nest/src/libs/migration-seed/FsMigrations.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import path from 'path';
|
||||
import { sortBy } from 'lodash';
|
||||
import fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { MigrateItem } from './interfaces';
|
||||
import { importWebpackSeedModule } from './Utils';
|
||||
import { DEFAULT_LOAD_EXTENSIONS } from './constants';
|
||||
import { filterMigrations } from './MigrateUtils';
|
||||
|
||||
const readdir = promisify(fs.readdir);
|
||||
|
||||
class FsMigrations {
|
||||
private sortDirsSeparately: boolean;
|
||||
private migrationsPaths: string[];
|
||||
private loadExtensions: string[];
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param migrationDirectories
|
||||
* @param sortDirsSeparately
|
||||
* @param loadExtensions
|
||||
*/
|
||||
constructor(
|
||||
migrationDirectories: string[],
|
||||
sortDirsSeparately: boolean,
|
||||
loadExtensions: string[]
|
||||
) {
|
||||
this.sortDirsSeparately = sortDirsSeparately;
|
||||
|
||||
if (!Array.isArray(migrationDirectories)) {
|
||||
migrationDirectories = [migrationDirectories];
|
||||
}
|
||||
this.migrationsPaths = migrationDirectories;
|
||||
this.loadExtensions = loadExtensions || DEFAULT_LOAD_EXTENSIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the migration names
|
||||
* @returns Promise<MigrateItem[]>
|
||||
*/
|
||||
public getMigrations(loadExtensions = null): Promise<MigrateItem[]> {
|
||||
// Get a list of files in all specified migration directories
|
||||
const readMigrationsPromises = this.migrationsPaths.map((configDir) => {
|
||||
const absoluteDir = path.resolve(process.cwd(), configDir);
|
||||
return readdir(absoluteDir).then((files) => ({
|
||||
files,
|
||||
configDir,
|
||||
absoluteDir,
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(readMigrationsPromises).then((allMigrations) => {
|
||||
const migrations = allMigrations.reduce((acc, migrationDirectory) => {
|
||||
// When true, files inside the folder should be sorted
|
||||
if (this.sortDirsSeparately) {
|
||||
migrationDirectory.files = migrationDirectory.files.sort();
|
||||
}
|
||||
migrationDirectory.files.forEach((file) =>
|
||||
acc.push({ file, directory: migrationDirectory.configDir })
|
||||
);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// If true we have already sorted the migrations inside the folders
|
||||
// return the migrations fully qualified
|
||||
if (this.sortDirsSeparately) {
|
||||
return filterMigrations(
|
||||
this,
|
||||
migrations,
|
||||
loadExtensions || this.loadExtensions
|
||||
);
|
||||
}
|
||||
return filterMigrations(
|
||||
this,
|
||||
sortBy(migrations, 'file'),
|
||||
loadExtensions || this.loadExtensions
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file name from given migrate item.
|
||||
* @param {MigrateItem} migration
|
||||
* @returns {string}
|
||||
*/
|
||||
public getMigrationName(migration: MigrateItem): string {
|
||||
return migration.file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the migrate file content from given migrate item.
|
||||
* @param {MigrateItem} migration
|
||||
* @returns {string}
|
||||
*/
|
||||
public getMigration(migration: MigrateItem): string {
|
||||
return importWebpackSeedModule(migration.file);
|
||||
}
|
||||
}
|
||||
|
||||
export { DEFAULT_LOAD_EXTENSIONS, FsMigrations };
|
||||
193
packages/server-nest/src/libs/migration-seed/MigrateUtils.ts
Normal file
193
packages/server-nest/src/libs/migration-seed/MigrateUtils.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
// @ts-nocheck
|
||||
import { differenceWith } from 'lodash';
|
||||
import path from 'path';
|
||||
import { FsMigrations } from './FsMigrations';
|
||||
import {
|
||||
getTable,
|
||||
getTableName,
|
||||
getLockTableName,
|
||||
getLockTableNameWithSchema,
|
||||
} from './TableUtils';
|
||||
import { ISeederConfig, MigrateItem } from './interfaces';
|
||||
|
||||
/**
|
||||
* Get schema-aware schema builder for a given schema nam
|
||||
* @param trxOrKnex
|
||||
* @param {string} schemaName
|
||||
* @returns
|
||||
*/
|
||||
function getSchemaBuilder(trxOrKnex, schemaName: string | null = null) {
|
||||
return schemaName
|
||||
? trxOrKnex.schema.withSchema(schemaName)
|
||||
: trxOrKnex.schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates migration table of the given table name.
|
||||
* @param {string} tableName
|
||||
* @param {string} schemaName
|
||||
* @param trxOrKnex
|
||||
* @returns
|
||||
*/
|
||||
function createMigrationTable(
|
||||
tableName: string,
|
||||
schemaName: string,
|
||||
trxOrKnex,
|
||||
) {
|
||||
return getSchemaBuilder(trxOrKnex, schemaName).createTable(
|
||||
getTableName(tableName),
|
||||
(t) => {
|
||||
t.increments();
|
||||
t.string('name');
|
||||
t.integer('batch');
|
||||
t.timestamp('migration_time');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a migration lock table of the given table name.
|
||||
* @param {string} tableName
|
||||
* @param {string} schemaName
|
||||
* @param trxOrKnex
|
||||
* @returns
|
||||
*/
|
||||
function createMigrationLockTable(
|
||||
tableName: string,
|
||||
schemaName: string,
|
||||
trxOrKnex,
|
||||
) {
|
||||
return getSchemaBuilder(trxOrKnex, schemaName).createTable(tableName, (t) => {
|
||||
t.increments('index').primary();
|
||||
t.integer('is_locked');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param tableName
|
||||
* @param schemaName
|
||||
* @param trxOrKnex
|
||||
* @returns
|
||||
*/
|
||||
export function ensureMigrationTables(
|
||||
tableName: string,
|
||||
schemaName: string,
|
||||
trxOrKnex,
|
||||
) {
|
||||
const lockTable = getLockTableName(tableName);
|
||||
const lockTableWithSchema = getLockTableNameWithSchema(tableName, schemaName);
|
||||
|
||||
return getSchemaBuilder(trxOrKnex, schemaName)
|
||||
.hasTable(tableName)
|
||||
.then((exists) => {
|
||||
return !exists && createMigrationTable(tableName, schemaName, trxOrKnex);
|
||||
})
|
||||
.then(() => {
|
||||
return getSchemaBuilder(trxOrKnex, schemaName).hasTable(lockTable);
|
||||
})
|
||||
.then((exists) => {
|
||||
return (
|
||||
!exists && createMigrationLockTable(lockTable, schemaName, trxOrKnex)
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
return getTable(trxOrKnex, lockTable, schemaName).select('*');
|
||||
})
|
||||
.then((data) => {
|
||||
return (
|
||||
!data.length &&
|
||||
trxOrKnex.into(lockTableWithSchema).insert({ is_locked: 0 })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all available migration versions, as a sorted array.
|
||||
* @param migrationSource
|
||||
* @param loadExtensions
|
||||
* @returns
|
||||
*/
|
||||
function listAll(
|
||||
migrationSource: FsMigrations,
|
||||
loadExtensions,
|
||||
): Promise<MigrateItem[]> {
|
||||
return migrationSource.getMigrations(loadExtensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all migrations that have been completed for the current db, as an array.
|
||||
* @param {string} tableName
|
||||
* @param {string} schemaName
|
||||
* @param {} trxOrKnex
|
||||
* @returns Promise<string[]>
|
||||
*/
|
||||
export async function listCompleted(
|
||||
tableName: string,
|
||||
schemaName: string,
|
||||
trxOrKnex,
|
||||
): Promise<string[]> {
|
||||
const completedMigrations = await trxOrKnex
|
||||
.from(getTableName(tableName, schemaName))
|
||||
.orderBy('id')
|
||||
.select('name');
|
||||
|
||||
return completedMigrations.map((migration) => {
|
||||
return migration.name;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the migration list from the migration directory specified in config, as well as
|
||||
* the list of completed migrations to check what should be run.
|
||||
*/
|
||||
export function listAllAndCompleted(config: ISeederConfig, trxOrKnex) {
|
||||
return Promise.all([
|
||||
listAll(config.migrationSource, config.loadExtensions),
|
||||
listCompleted(config.tableName, config.schemaName, trxOrKnex),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param migrationSource
|
||||
* @param all
|
||||
* @param completed
|
||||
* @returns
|
||||
*/
|
||||
export function getNewMigrations(
|
||||
migrationSource: FsMigrations,
|
||||
all: MigrateItem[],
|
||||
completed: string[],
|
||||
): MigrateItem[] {
|
||||
return differenceWith(all, completed, (allMigration, completedMigration) => {
|
||||
return (
|
||||
completedMigration === migrationSource.getMigrationName(allMigration)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function startsWithNumber(str) {
|
||||
return /^\d/.test(str);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {FsMigrations} migrationSource -
|
||||
* @param {MigrateItem[]} migrations -
|
||||
* @param {string[]} loadExtensions -
|
||||
* @returns
|
||||
*/
|
||||
export function filterMigrations(
|
||||
migrationSource: FsMigrations,
|
||||
migrations: MigrateItem[],
|
||||
loadExtensions: string[],
|
||||
) {
|
||||
return migrations.filter((migration) => {
|
||||
const migrationName = migrationSource.getMigrationName(migration);
|
||||
const extension = path.extname(migrationName);
|
||||
|
||||
return (
|
||||
loadExtensions.includes(extension) && startsWithNumber(migrationName)
|
||||
);
|
||||
});
|
||||
}
|
||||
223
packages/server-nest/src/libs/migration-seed/SeedMigration.ts
Normal file
223
packages/server-nest/src/libs/migration-seed/SeedMigration.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
// @ts-nocheck
|
||||
import { Knex } from 'knex';
|
||||
import Bluebird from 'bluebird';
|
||||
import { getTable, getTableName, getLockTableName } from './TableUtils';
|
||||
import getMergedConfig from './SeederConfig';
|
||||
import {
|
||||
listAllAndCompleted,
|
||||
getNewMigrations,
|
||||
listCompleted,
|
||||
ensureMigrationTables,
|
||||
} from './MigrateUtils';
|
||||
import { MigrateItem, SeedMigrationContext, ISeederConfig } from './interfaces';
|
||||
import { FsMigrations } from './FsMigrations';
|
||||
|
||||
export class SeedMigration {
|
||||
knex: Knex;
|
||||
config: ISeederConfig;
|
||||
migrationSource: FsMigrations;
|
||||
context: SeedMigrationContext;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {Knex} knex - Knex instance.
|
||||
* @param {SeedMigrationContext} context -
|
||||
*/
|
||||
constructor(knex: Knex, context: SeedMigrationContext) {
|
||||
this.knex = knex;
|
||||
this.config = getMergedConfig(this.knex.client.config.seeds, undefined);
|
||||
this.migrationSource = this.config.migrationSource;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest migration.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async latest(config = null): Promise<void> {
|
||||
// Merges the configuration.
|
||||
this.config = getMergedConfig(config, this.config);
|
||||
|
||||
// Ensure migration tables.
|
||||
await ensureMigrationTables(this.config.tableName, null, this.knex);
|
||||
|
||||
// Retrieve all and completed migrations.
|
||||
const [all, completed] = await listAllAndCompleted(this.config, this.knex);
|
||||
|
||||
// Retrieve the new migrations.
|
||||
const migrations = getNewMigrations(this.migrationSource, all, completed);
|
||||
|
||||
// Run the latest migration on one batch.
|
||||
return this.knex.transaction((trx: Knex.Transaction) => {
|
||||
return this.runBatch(migrations, 'up', trx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add migration lock flag.
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
private migrateLockTable(trx: Knex.Transaction) {
|
||||
const tableName = getLockTableName(this.config.tableName);
|
||||
return getTable(this.knex, tableName, this.config.schemaName)
|
||||
.transacting(trx)
|
||||
.where('is_locked', '=', 0)
|
||||
.update({ is_locked: 1 })
|
||||
.then((rowCount) => {
|
||||
if (rowCount != 1) {
|
||||
throw new Error('Migration table is already locked');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add migration lock flag.
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
private migrationLock(trx: Knex.Transaction) {
|
||||
return this.migrateLockTable(trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the migration lock flag.
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
private freeLock(trx = this.knex): Promise<void> {
|
||||
const tableName = getLockTableName(this.config.tableName);
|
||||
|
||||
return getTable(trx, tableName, this.config.schemaName).update({
|
||||
is_locked: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest batch number.
|
||||
* @param trx
|
||||
* @returns
|
||||
*/
|
||||
private latestBatchNumber(trx = this.knex): number {
|
||||
return trx
|
||||
.from(getTableName(this.config.tableName, this.config.schemaName))
|
||||
.max('batch as max_batch')
|
||||
.then((obj) => obj[0].max_batch || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a batch of `migrations` in a specified `direction`, saving the
|
||||
* appropriate database information as the migrations are run.
|
||||
* @param {number} batchNo
|
||||
* @param {MigrateItem[]} migrations
|
||||
* @param {string} direction
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private waterfallBatch(
|
||||
batchNo: number,
|
||||
migrations: MigrateItem[],
|
||||
direction: string,
|
||||
trx: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const { tableName } = this.config;
|
||||
|
||||
return Bluebird.each(migrations, (migration) => {
|
||||
const name = this.migrationSource.getMigrationName(migration);
|
||||
|
||||
return this.migrationSource
|
||||
.getMigration(migration)
|
||||
.then((migrationContent) =>
|
||||
this.runMigrationContent(migrationContent.default, direction, trx),
|
||||
)
|
||||
.then(() => {
|
||||
if (direction === 'up') {
|
||||
return trx.into(getTableName(tableName)).insert({
|
||||
name,
|
||||
batch: batchNo,
|
||||
migration_time: new Date(),
|
||||
});
|
||||
}
|
||||
if (direction === 'down') {
|
||||
return trx.from(getTableName(tableName)).where({ name }).del();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs and builds the given migration class.
|
||||
*/
|
||||
private runMigrationContent(Migration, direction, trx) {
|
||||
const instance = new Migration(trx);
|
||||
|
||||
if (this.context.i18n) {
|
||||
instance.setI18n(this.context.i18n);
|
||||
}
|
||||
instance.setTenant(this.context.tenant);
|
||||
|
||||
return instance[direction](trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates some migrations by requiring and checking for an `up` and `down`function.
|
||||
* @param {MigrateItem} migration
|
||||
* @returns {MigrateItem}
|
||||
*/
|
||||
async validateMigrationStructure(migration: MigrateItem): MigrateItem {
|
||||
const migrationName = this.migrationSource.getMigrationName(migration);
|
||||
|
||||
// maybe promise
|
||||
const migrationContent = await this.migrationSource.getMigration(migration);
|
||||
if (
|
||||
typeof migrationContent.up !== 'function' ||
|
||||
typeof migrationContent.down !== 'function'
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid migration: ${migrationName} must have both an up and down function`,
|
||||
);
|
||||
}
|
||||
return migration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a batch of current migrations, in sequence.
|
||||
* @param {MigrateItem[]} migrations
|
||||
* @param {string} direction
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async runBatch(
|
||||
migrations: MigrateItem[],
|
||||
direction: string,
|
||||
trx: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Adds flag to migration lock.
|
||||
await this.migrationLock(trx);
|
||||
|
||||
// When there is a wrapping transaction, some migrations
|
||||
// could have been done while waiting for the lock:
|
||||
const completed = await listCompleted(
|
||||
this.config.tableName,
|
||||
this.config.schemaName,
|
||||
trx,
|
||||
);
|
||||
// Differentiate between all and completed to get new migrations.
|
||||
const newMigrations = getNewMigrations(
|
||||
this.config.migrationSource,
|
||||
migrations,
|
||||
completed,
|
||||
);
|
||||
// Retrieve the latest batch number.
|
||||
const batchNo = await this.latestBatchNumber(trx);
|
||||
|
||||
// Increment the next batch number.
|
||||
const newBatchNo = direction === 'up' ? batchNo + 1 : batchNo;
|
||||
|
||||
// Run all migration files in waterfall.
|
||||
await this.waterfallBatch(newBatchNo, newMigrations, direction, trx);
|
||||
|
||||
// Free the migration lock flag.
|
||||
await this.freeLock(trx);
|
||||
}
|
||||
}
|
||||
11
packages/server-nest/src/libs/migration-seed/Seeder.ts
Normal file
11
packages/server-nest/src/libs/migration-seed/Seeder.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
export class Seeder {
|
||||
knex: any;
|
||||
|
||||
constructor(knex) {
|
||||
this.knex = knex;
|
||||
}
|
||||
up(knex) {}
|
||||
down(knex) {}
|
||||
}
|
||||
|
||||
44
packages/server-nest/src/libs/migration-seed/SeederConfig.ts
Normal file
44
packages/server-nest/src/libs/migration-seed/SeederConfig.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { DEFAULT_LOAD_EXTENSIONS, FsMigrations } from './FsMigrations';
|
||||
|
||||
const CONFIG_DEFAULT = Object.freeze({
|
||||
extension: 'js',
|
||||
loadExtensions: DEFAULT_LOAD_EXTENSIONS,
|
||||
tableName: 'knex_migrations',
|
||||
schemaName: null,
|
||||
directory: './migrations',
|
||||
disableTransactions: false,
|
||||
disableMigrationsListValidation: false,
|
||||
sortDirsSeparately: false,
|
||||
});
|
||||
|
||||
export default function getMergedConfig(config, currentConfig) {
|
||||
// config is the user specified config, mergedConfig has defaults and current config
|
||||
// applied to it.
|
||||
const mergedConfig = {
|
||||
...CONFIG_DEFAULT,
|
||||
...(currentConfig || {}),
|
||||
...config,
|
||||
};
|
||||
|
||||
if (
|
||||
config &&
|
||||
// If user specifies any FS related config,
|
||||
// clear specified migrationSource to avoid ambiguity
|
||||
(config.directory ||
|
||||
config.sortDirsSeparately !== undefined ||
|
||||
config.loadExtensions)
|
||||
) {
|
||||
mergedConfig.migrationSource = null;
|
||||
}
|
||||
|
||||
// If the user has not specified any configs, we need to
|
||||
// default to fs migrations to maintain compatibility
|
||||
if (!mergedConfig.migrationSource) {
|
||||
mergedConfig.migrationSource = new FsMigrations(
|
||||
mergedConfig.directory,
|
||||
mergedConfig.sortDirsSeparately,
|
||||
mergedConfig.loadExtensions
|
||||
);
|
||||
}
|
||||
return mergedConfig;
|
||||
}
|
||||
43
packages/server-nest/src/libs/migration-seed/TableUtils.ts
Normal file
43
packages/server-nest/src/libs/migration-seed/TableUtils.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Get schema-aware query builder for a given table and schema name.
|
||||
* @param {Knex} trxOrKnex -
|
||||
* @param {string} tableName -
|
||||
* @param {string} schemaName -
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getTable(trx, tableName: string, schemaName = null) {
|
||||
return schemaName ? trx(tableName).withSchema(schemaName) : trx(tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schema-aware table name.
|
||||
* @param {string} tableName -
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getTableName(tableName: string, schemaName = null): string {
|
||||
return schemaName ? `${schemaName}.${tableName}` : tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the lock table name from given migration table name.
|
||||
* @param {string} tableName
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getLockTableName(tableName: string): string {
|
||||
return `${tableName}_lock`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retireve the lock table name from ginve migration table name with schema.
|
||||
* @param {string} tableName
|
||||
* @param {string} schemaName
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getLockTableNameWithSchema(
|
||||
tableName: string,
|
||||
schemaName = null
|
||||
): string {
|
||||
return schemaName
|
||||
? `${schemaName} + ${getLockTableName(tableName)}`
|
||||
: getLockTableName(tableName);
|
||||
}
|
||||
25
packages/server-nest/src/libs/migration-seed/TenantSeeder.ts
Normal file
25
packages/server-nest/src/libs/migration-seed/TenantSeeder.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Seeder } from "./Seeder";
|
||||
|
||||
export class TenantSeeder extends Seeder{
|
||||
public knex: any;
|
||||
public i18n: i18nAPI;
|
||||
public models: any;
|
||||
public tenant: any;
|
||||
|
||||
constructor(knex) {
|
||||
super(knex);
|
||||
this.knex = knex;
|
||||
}
|
||||
|
||||
setI18n(i18n) {
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
setModels(models) {
|
||||
this.models = models;
|
||||
}
|
||||
|
||||
setTenant(tenant) {
|
||||
this.tenant = tenant;
|
||||
}
|
||||
}
|
||||
43
packages/server-nest/src/libs/migration-seed/Utils.ts
Normal file
43
packages/server-nest/src/libs/migration-seed/Utils.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// @ts-nocheck
|
||||
import fs from 'fs';
|
||||
|
||||
const { promisify } = require('util');
|
||||
const readFile = promisify(fs.readFile);
|
||||
|
||||
/**
|
||||
* Detarmines the module type of the given file path.
|
||||
* @param {string} filepath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
async function isModuleType(filepath: string): boolean {
|
||||
if (process.env.npm_package_json) {
|
||||
// npm >= 7.0.0
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(process.env.npm_package_json, 'utf-8'),
|
||||
);
|
||||
if (packageJson.type === 'module') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return process.env.npm_package_type === 'module' || filepath.endsWith('.mjs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports content of the given file path.
|
||||
* @param {string} filepath
|
||||
* @returns
|
||||
*/
|
||||
export async function importFile(filepath: string): any {
|
||||
return (await isModuleType(filepath))
|
||||
? import(require('url').pathToFileURL(filepath))
|
||||
: require(filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} moduleName
|
||||
* @returns
|
||||
*/
|
||||
export async function importWebpackSeedModule(moduleName: string): any {
|
||||
return import(`@/database/seeds/core/${moduleName}`);
|
||||
}
|
||||
12
packages/server-nest/src/libs/migration-seed/constants.ts
Normal file
12
packages/server-nest/src/libs/migration-seed/constants.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Default load extensions.
|
||||
export const DEFAULT_LOAD_EXTENSIONS = [
|
||||
'.co',
|
||||
'.coffee',
|
||||
'.eg',
|
||||
'.iced',
|
||||
'.js',
|
||||
'.cjs',
|
||||
'.litcoffee',
|
||||
'.ls',
|
||||
'.ts',
|
||||
];
|
||||
20
packages/server-nest/src/libs/migration-seed/interfaces.ts
Normal file
20
packages/server-nest/src/libs/migration-seed/interfaces.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||
|
||||
export interface FsMigrations {}
|
||||
|
||||
export interface ISeederConfig {
|
||||
tableName: string;
|
||||
migrationSource: FsMigrations;
|
||||
schemaName?: string;
|
||||
loadExtensions: string[];
|
||||
}
|
||||
|
||||
export interface MigrateItem {
|
||||
file: string;
|
||||
directory: string;
|
||||
}
|
||||
|
||||
export interface SeedMigrationContext {
|
||||
i18n: any;
|
||||
tenant: TenantModel;
|
||||
}
|
||||
@@ -190,7 +190,7 @@ import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module
|
||||
RolesModule,
|
||||
SubscriptionModule,
|
||||
OrganizationModule,
|
||||
TenantDBManagerModule
|
||||
TenantDBManagerModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
|
||||
import { DashboardService } from './Dashboard.service';
|
||||
import { FeaturesModule } from '../Features/Features.module';
|
||||
import { DashboardController } from './Dashboard.controller';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
|
||||
@Module({
|
||||
imports: [FeaturesModule],
|
||||
providers: [DashboardService],
|
||||
providers: [DashboardService, TenancyContext],
|
||||
controllers: [DashboardController],
|
||||
})
|
||||
export class DashboardModule {}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { omit, sumBy } from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IManualJournalDTO,
|
||||
IManualJournalEventEditedPayload,
|
||||
IManualJournalEditingPayload,
|
||||
} from '../types/ManualJournals.types';
|
||||
|
||||
@@ -6,13 +6,15 @@ import { OrganizationController } from './Organization.controller';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { OrganizationBuildQueue } from './Organization.types';
|
||||
import { OrganizationBuildProcessor } from './processors/OrganizationBuild.processor';
|
||||
import { CommandOrganizationValidators } from './commands/CommandOrganizationValidators.service';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
GetCurrentOrganizationService,
|
||||
BuildOrganizationService,
|
||||
UpdateOrganizationService,
|
||||
OrganizationBuildProcessor
|
||||
OrganizationBuildProcessor,
|
||||
CommandOrganizationValidators,
|
||||
],
|
||||
imports: [BullModule.registerQueue({ name: OrganizationBuildQueue })],
|
||||
controllers: [OrganizationController],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { TenantJobPayload } from '@/interfaces/Tenant';
|
||||
import { SystemUser } from '../System/models/SystemUser';
|
||||
import { BuildOrganizationDto } from './dtos/Organization.dto';
|
||||
|
||||
export interface IOrganizationSetupDTO {
|
||||
organizationName: string;
|
||||
@@ -53,6 +55,6 @@ export interface IOrganizationBuiltEventPayload {
|
||||
export const OrganizationBuildQueue = 'OrganizationBuildQueue';
|
||||
export const OrganizationBuildQueueJob = 'OrganizationBuildQueueJob';
|
||||
|
||||
export interface OrganizationBuildQueueJobPayload {
|
||||
buildDto: IOrganizationBuildDTO;
|
||||
}
|
||||
export interface OrganizationBuildQueueJobPayload extends TenantJobPayload {
|
||||
buildDto: BuildOrganizationDto;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defaultTo } from 'lodash';
|
||||
import { IOrganizationBuildDTO } from './Organization.types';
|
||||
import { BuildOrganizationDto } from './dtos/Organization.dto';
|
||||
|
||||
/**
|
||||
* Transformes build DTO object.
|
||||
@@ -7,8 +8,8 @@ import { IOrganizationBuildDTO } from './Organization.types';
|
||||
* @returns {IOrganizationBuildDTO}
|
||||
*/
|
||||
export const transformBuildDto = (
|
||||
buildDTO: IOrganizationBuildDTO,
|
||||
): IOrganizationBuildDTO => {
|
||||
buildDTO: BuildOrganizationDto,
|
||||
): BuildOrganizationDto => {
|
||||
return {
|
||||
...buildDTO,
|
||||
dateFormat: defaultTo(buildDTO.dateFormat, 'DD MMM yyyy'),
|
||||
|
||||
@@ -1,57 +1,62 @@
|
||||
import { Queue } from 'bullmq';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import {
|
||||
IOrganizationBuildDTO,
|
||||
IOrganizationBuildEventPayload,
|
||||
IOrganizationBuiltEventPayload,
|
||||
OrganizationBuildQueue,
|
||||
OrganizationBuildQueueJob,
|
||||
OrganizationBuildQueueJobPayload,
|
||||
} from '../Organization.types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { throwIfTenantInitizalized, throwIfTenantIsBuilding } from '../Organization/_utils';
|
||||
import {
|
||||
throwIfTenantInitizalized,
|
||||
throwIfTenantIsBuilding,
|
||||
} from '../Organization/_utils';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { TenantsManagerService } from '@/modules/TenantDBManager/TenantsManager';
|
||||
import { events } from '@/common/events/events';
|
||||
import { transformBuildDto } from '../Organization.utils';
|
||||
import { BuildOrganizationDto } from '../dtos/Organization.dto';
|
||||
import { TenantRepository } from '@/modules/System/repositories/Tenant.repository';
|
||||
|
||||
@Injectable()
|
||||
export class BuildOrganizationService {
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly tenantsManager: TenantsManagerService,
|
||||
private readonly tenancyContext: TenancyContext
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
private readonly tenantRepository: TenantRepository,
|
||||
|
||||
@InjectQueue(OrganizationBuildQueue)
|
||||
private readonly computeItemCostProcessor: Queue,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Builds the database schema and seed data of the given organization id.
|
||||
* @param {srting} organizationId
|
||||
* @param {string} organizationId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async build(
|
||||
buildDTO: BuildOrganizationDto,
|
||||
): Promise<void> {
|
||||
public async build(buildDTO: BuildOrganizationDto): Promise<void> {
|
||||
const tenant = await this.tenancyContext.getTenant();
|
||||
const systemUser = await this.tenancyContext.getSystemUser();
|
||||
|
||||
// Throw error if the tenant is already initialized.
|
||||
throwIfTenantInitizalized(tenant);
|
||||
|
||||
// Drop the database if is already exists.
|
||||
await this.tenantsManager.dropDatabaseIfExists(tenant);
|
||||
|
||||
// Creates a new database.
|
||||
await this.tenantsManager.createDatabase(tenant);
|
||||
|
||||
// Migrate the tenant.
|
||||
await this.tenantsManager.migrateTenant(tenant);
|
||||
|
||||
// Migrated tenant.
|
||||
const migratedTenant = await tenant.$query().withGraphFetched('metadata');
|
||||
|
||||
// Creates a tenancy object from given tenant model.
|
||||
const tenancyContext =
|
||||
this.tenantsManager.getSeedMigrationContext(migratedTenant);
|
||||
// const tenancyContext =
|
||||
// this.tenantsManager.getSeedMigrationContext(migratedTenant);
|
||||
|
||||
// Seed tenant.
|
||||
await this.tenantsManager.seedTenant(migratedTenant, tenancyContext);
|
||||
await this.tenantsManager.seedTenant(migratedTenant, {});
|
||||
|
||||
// Throws `onOrganizationBuild` event.
|
||||
await this.eventPublisher.emitAsync(events.organization.build, {
|
||||
@@ -60,12 +65,12 @@ export class BuildOrganizationService {
|
||||
systemUser,
|
||||
} as IOrganizationBuildEventPayload);
|
||||
|
||||
// Markes the tenant as completed builing.
|
||||
await Tenant.markAsBuilt(tenantId);
|
||||
await Tenant.markAsBuildCompleted(tenantId);
|
||||
// Marks the tenant as completed builing.
|
||||
await this.tenantRepository.markAsBuilt().findById(tenant.id);
|
||||
await this.tenantRepository.markAsBuildCompleted().findById(tenant.id);
|
||||
|
||||
//
|
||||
await this.flagTenantDBBatch(tenantId);
|
||||
// Flags the tenant database batch.
|
||||
await this.tenantRepository.flagTenantDBBatch().findById(tenant.id);
|
||||
|
||||
// Triggers the organization built event.
|
||||
await this.eventPublisher.emitAsync(events.organization.built, {
|
||||
@@ -75,13 +80,12 @@ export class BuildOrganizationService {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {IOrganizationBuildDTO} buildDTO
|
||||
* @returns
|
||||
* @param {BuildOrganizationDto} buildDTO
|
||||
* @returns {Promise<{ nextRunAt: Date; jobId: string }>} - Returns the next run date and job id.
|
||||
*/
|
||||
async buildRunJob(
|
||||
buildDTO: BuildOrganizationDto,
|
||||
) {
|
||||
): Promise<{ nextRunAt: Date; jobId: string }> {
|
||||
const tenant = await this.tenancyContext.getTenant();
|
||||
const systemUser = await this.tenancyContext.getSystemUser();
|
||||
|
||||
@@ -91,27 +95,26 @@ export class BuildOrganizationService {
|
||||
// Throw error if tenant is currently building.
|
||||
throwIfTenantIsBuilding(tenant);
|
||||
|
||||
// Transformes build DTO object.
|
||||
// Transforms build DTO object.
|
||||
const transformedBuildDTO = transformBuildDto(buildDTO);
|
||||
|
||||
// Saves the tenant metadata.
|
||||
await tenant.saveMetadata(transformedBuildDTO);
|
||||
await this.tenantRepository.saveMetadata(tenant.id, transformedBuildDTO);
|
||||
|
||||
// Send welcome mail to the user.
|
||||
const jobMeta = await this.agenda.now('organization-setup', {
|
||||
tenantId,
|
||||
buildDTO,
|
||||
authorizedUser,
|
||||
});
|
||||
// Transformes the mangodb id to string.
|
||||
const jobId = new ObjectId(jobMeta.attrs._id).toString();
|
||||
|
||||
// Markes the tenant as currently building.
|
||||
await Tenant.markAsBuilding(tenantId, jobId);
|
||||
const jobMeta = await this.computeItemCostProcessor.add(
|
||||
OrganizationBuildQueueJob,
|
||||
{
|
||||
organizationId: tenant.organizationId,
|
||||
userId: systemUser.id,
|
||||
buildDto: transformedBuildDTO,
|
||||
} as OrganizationBuildQueueJobPayload,
|
||||
);
|
||||
// Marks the tenant as currently building.
|
||||
await this.tenantRepository.markAsBuilding(jobMeta.id).findById(tenant.id);
|
||||
|
||||
return {
|
||||
nextRunAt: jobMeta.attrs.nextRunAt,
|
||||
jobId: jobMeta.attrs._id,
|
||||
nextRunAt: jobMeta.data.nextRunAt,
|
||||
jobId: jobMeta.data.id,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,16 +126,4 @@ export class BuildOrganizationService {
|
||||
public async revertBuildRunJob() {
|
||||
// await Tenant.markAsBuildCompleted(tenantId, jobId);
|
||||
}
|
||||
/**
|
||||
* Adds organization database latest batch number.
|
||||
* @param {number} tenantId
|
||||
* @param {number} version
|
||||
*/
|
||||
public async flagTenantDBBatch(tenantId: number) {
|
||||
await Tenant.query()
|
||||
.update({
|
||||
databaseBatch: config.databaseBatch,
|
||||
})
|
||||
.where({ id: tenantId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { OrganizationBaseCurrencyLocking } from '../Organization/OrganizationBaseCurrencyLocking.service';
|
||||
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ERRORS } from '../Organization.constants';
|
||||
|
||||
@Injectable()
|
||||
export class CommandOrganizationValidators {
|
||||
constructor(
|
||||
private readonly baseCurrencyMutateLocking: OrganizationBaseCurrencyLocking,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Throw base currency mutate locked error.
|
||||
*/
|
||||
throwBaseCurrencyMutateLocked() {
|
||||
throw new ServiceError(ERRORS.BASE_CURRENCY_MUTATE_LOCKED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate mutate base currency ability.
|
||||
* @param {Tenant} tenant -
|
||||
* @param {string} newBaseCurrency -
|
||||
* @param {string} oldBaseCurrency -
|
||||
*/
|
||||
async validateMutateBaseCurrency(
|
||||
tenant: TenantModel,
|
||||
newBaseCurrency: string,
|
||||
oldBaseCurrency: string,
|
||||
) {
|
||||
if (tenant.isReady && newBaseCurrency !== oldBaseCurrency) {
|
||||
const isLocked =
|
||||
await this.baseCurrencyMutateLocking.isBaseCurrencyMutateLocked(
|
||||
tenant.id,
|
||||
);
|
||||
|
||||
if (isLocked) {
|
||||
this.throwBaseCurrencyMutateLocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,27 @@
|
||||
import { TenancyContext } from "@/modules/Tenancy/TenancyContext.service";
|
||||
import { UpdateOrganizationDto } from "../dtos/Organization.dto";
|
||||
import { throwIfTenantNotExists } from "../Organization/_utils";
|
||||
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { UpdateOrganizationDto } from '../dtos/Organization.dto';
|
||||
import { throwIfTenantNotExists } from '../Organization/_utils';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { CommandOrganizationValidators } from './CommandOrganizationValidators.service';
|
||||
import { TenantRepository } from '@/modules/System/repositories/Tenant.repository';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateOrganizationService {
|
||||
constructor(
|
||||
private readonly tenancyContext: TenancyContext
|
||||
) {
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly commandOrganizationValidators: CommandOrganizationValidators,
|
||||
|
||||
}
|
||||
private readonly tenantRepository: TenantRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Updates organization information.
|
||||
* @param {ITenant} tenantId
|
||||
* @param {IOrganizationUpdateDTO} organizationDTO
|
||||
* @param {UpdateOrganizationDto} organizationDTO
|
||||
*/
|
||||
public async execute(
|
||||
organizationDTO: UpdateOrganizationDto,
|
||||
): Promise<void> {
|
||||
public async execute(organizationDTO: UpdateOrganizationDto): Promise<void> {
|
||||
const tenant = await this.tenancyContext.getTenant(true);
|
||||
|
||||
// Throw error if the tenant not exists.
|
||||
@@ -25,23 +29,22 @@ export class UpdateOrganizationService {
|
||||
|
||||
// Validate organization transactions before mutate base currency.
|
||||
if (organizationDTO.baseCurrency) {
|
||||
await this.validateMutateBaseCurrency(
|
||||
await this.commandOrganizationValidators.validateMutateBaseCurrency(
|
||||
tenant,
|
||||
organizationDTO.baseCurrency,
|
||||
tenant.metadata?.baseCurrency,
|
||||
);
|
||||
}
|
||||
await tenant.saveMetadata(organizationDTO);
|
||||
await this.tenantRepository.saveMetadata(tenant.id, organizationDTO);
|
||||
|
||||
if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) {
|
||||
// Triggers `onOrganizationBaseCurrencyUpdated` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
await this.eventEmitter.emitAsync(
|
||||
events.organization.baseCurrencyUpdated,
|
||||
{
|
||||
tenantId,
|
||||
organizationDTO,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import {IsHexColor,
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
IsHexColor,
|
||||
IsIn,
|
||||
IsISO31661Alpha2,
|
||||
IsISO4217CurrencyCode,
|
||||
@@ -8,69 +9,145 @@ import {IsHexColor,
|
||||
} from 'class-validator';
|
||||
import { MONTHS } from '../Organization/constants';
|
||||
import { ACCEPTED_LOCALES, DATE_FORMATS } from '../Organization.constants';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class BuildOrganizationDto {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Organization name',
|
||||
example: 'Acme Inc.',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Industry of the organization',
|
||||
example: 'Technology',
|
||||
})
|
||||
industry?: string;
|
||||
|
||||
@IsISO31661Alpha2()
|
||||
@ApiProperty({
|
||||
description: 'Country location in ISO 3166-1 alpha-2 format',
|
||||
example: 'US',
|
||||
})
|
||||
location: string;
|
||||
|
||||
@IsISO4217CurrencyCode()
|
||||
@ApiProperty({
|
||||
description: 'Base currency in ISO 4217 format',
|
||||
example: 'USD',
|
||||
})
|
||||
baseCurrency: string;
|
||||
|
||||
@IsIn(moment.tz.names())
|
||||
@ApiProperty({
|
||||
description: 'Timezone of the organization',
|
||||
example: 'America/New_York',
|
||||
})
|
||||
timezone: string;
|
||||
|
||||
@IsIn(MONTHS)
|
||||
@ApiProperty({
|
||||
description: 'Starting month of fiscal year',
|
||||
example: 'January',
|
||||
})
|
||||
fiscalYear: string;
|
||||
|
||||
@IsIn(ACCEPTED_LOCALES)
|
||||
@ApiProperty({
|
||||
description: 'Language/locale of the organization',
|
||||
example: 'en-US',
|
||||
})
|
||||
language: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(DATE_FORMATS)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Date format used by the organization',
|
||||
example: 'MM/DD/YYYY',
|
||||
})
|
||||
dateFormat?: string;
|
||||
}
|
||||
|
||||
export class UpdateOrganizationDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Organization name',
|
||||
example: 'Acme Inc.',
|
||||
})
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Industry of the organization',
|
||||
example: 'Technology',
|
||||
})
|
||||
industry?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO31661Alpha2()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Country location in ISO 3166-1 alpha-2 format',
|
||||
example: 'US',
|
||||
})
|
||||
location?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO4217CurrencyCode()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Base currency in ISO 4217 format',
|
||||
example: 'USD',
|
||||
})
|
||||
baseCurrency?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(moment.tz.names())
|
||||
@ApiPropertyOptional({
|
||||
description: 'Timezone of the organization',
|
||||
example: 'America/New_York',
|
||||
})
|
||||
timezone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(MONTHS)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Starting month of fiscal year',
|
||||
example: 'January',
|
||||
})
|
||||
fiscalYear?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(ACCEPTED_LOCALES)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Language/locale of the organization',
|
||||
example: 'en-US',
|
||||
})
|
||||
language?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(DATE_FORMATS)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Date format used by the organization',
|
||||
example: 'MM/DD/YYYY',
|
||||
})
|
||||
dateFormat?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Organization address details',
|
||||
example: {
|
||||
address_1: '123 Main St',
|
||||
address_2: 'Suite 100',
|
||||
postal_code: '10001',
|
||||
city: 'New York',
|
||||
stateProvince: 'NY',
|
||||
phone: '+1-555-123-4567',
|
||||
},
|
||||
})
|
||||
address?: {
|
||||
address_1?: string;
|
||||
address_2?: string;
|
||||
@@ -82,13 +159,25 @@ export class UpdateOrganizationDto {
|
||||
|
||||
@IsOptional()
|
||||
@IsHexColor()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Primary brand color in hex format',
|
||||
example: '#4285F4',
|
||||
})
|
||||
primaryColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Logo file key reference',
|
||||
example: 'organizations/acme-logo-123456.png',
|
||||
})
|
||||
logoKey?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: 'Organization tax identification number',
|
||||
example: '12-3456789',
|
||||
})
|
||||
taxNumber?: string;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ServiceError } from '../Items/ServiceError';
|
||||
import { GetInvoicePaymentLinkMetaTransformer } from '../SaleInvoices/queries/GetInvoicePaymentLink.transformer';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { TenantModel } from '../System/models/TenantModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetInvoicePaymentLinkMetadata {
|
||||
@@ -21,6 +22,9 @@ export class GetInvoicePaymentLinkMetadata {
|
||||
|
||||
@Inject(PaymentLink.name)
|
||||
private readonly paymentLinkModel: typeof PaymentLink,
|
||||
|
||||
@Inject(TenantModel.name)
|
||||
private readonly systemTenantModel: typeof TenantModel,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -28,7 +32,8 @@ export class GetInvoicePaymentLinkMetadata {
|
||||
* @param {string} linkId - Link id.
|
||||
*/
|
||||
async getInvoicePaymentLinkMeta(linkId: string) {
|
||||
const paymentLink = await this.paymentLinkModel.query()
|
||||
const paymentLink = await this.paymentLinkModel
|
||||
.query()
|
||||
.findOne('linkId', linkId)
|
||||
.where('resourceType', 'SaleInvoice')
|
||||
.throwIfNotFound();
|
||||
@@ -42,8 +47,12 @@ export class GetInvoicePaymentLinkMetadata {
|
||||
throw new ServiceError('PAYMENT_LINK_EXPIRED');
|
||||
}
|
||||
}
|
||||
this.clsService.set('organizationId', paymentLink.tenantId);
|
||||
this.clsService.set('userId', paymentLink.userId);
|
||||
const tenant = await this.systemTenantModel
|
||||
.query()
|
||||
.findById(paymentLink.tenantId);
|
||||
|
||||
this.clsService.set('organizationId', tenant.organizationId);
|
||||
// this.clsService.set('userId', paymentLink.userId);
|
||||
|
||||
const invoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SaleInvoicePdf } from '../SaleInvoices/queries/SaleInvoicePdf.service';
|
||||
import { PaymentLink } from './models/PaymentLink';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { TenantModel } from '../System/models/TenantModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetPaymentLinkInvoicePdf {
|
||||
constructor(
|
||||
private readonly getSaleInvoicePdfService: SaleInvoicePdf,
|
||||
private readonly clsService: ClsService,
|
||||
|
||||
@Inject(PaymentLink.name)
|
||||
private readonly paymentLinkModel: typeof PaymentLink,
|
||||
|
||||
@Inject(TenantModel.name)
|
||||
private readonly systemTenantModel: typeof TenantModel,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -19,15 +25,18 @@ export class GetPaymentLinkInvoicePdf {
|
||||
async getPaymentLinkInvoicePdf(
|
||||
paymentLinkId: string,
|
||||
): Promise<[Buffer, string]> {
|
||||
const paymentLink = await this.paymentLinkModel.query()
|
||||
const paymentLink = await this.paymentLinkModel
|
||||
.query()
|
||||
.findOne('linkId', paymentLinkId)
|
||||
.where('resourceType', 'SaleInvoice')
|
||||
.throwIfNotFound();
|
||||
|
||||
const tenantId = paymentLink.tenantId;
|
||||
await initalizeTenantServices(tenantId);
|
||||
|
||||
const saleInvoiceId = paymentLink.resourceId;
|
||||
const tenant = await this.systemTenantModel
|
||||
.query()
|
||||
.findById(paymentLink.tenantId);
|
||||
|
||||
this.clsService.set('organizationId', tenant.organizationId);
|
||||
|
||||
return this.getSaleInvoicePdfService.getSaleInvoicePdf(saleInvoiceId);
|
||||
}
|
||||
|
||||
@@ -6,16 +6,21 @@ import { PaymentLinksController } from './PaymentLinks.controller';
|
||||
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
|
||||
import { PaymentLink } from './models/PaymentLink';
|
||||
import { StripePaymentModule } from '../StripePayment/StripePayment.module';
|
||||
import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module';
|
||||
import { GetInvoicePaymentLinkMetadata } from './GetInvoicePaymentLinkMetadata';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
|
||||
const models = [InjectSystemModel(PaymentLink)];
|
||||
|
||||
@Module({
|
||||
imports: [StripePaymentModule],
|
||||
imports: [StripePaymentModule, SaleInvoicesModule],
|
||||
providers: [
|
||||
...models,
|
||||
TenancyContext,
|
||||
CreateInvoiceCheckoutSession,
|
||||
GetPaymentLinkInvoicePdf,
|
||||
PaymentLinksApplication,
|
||||
GetInvoicePaymentLinkMetadata,
|
||||
],
|
||||
controllers: [PaymentLinksController],
|
||||
exports: [...models, PaymentLinksApplication],
|
||||
|
||||
13
packages/server-nest/src/modules/Roles/Roles.utils.ts
Normal file
13
packages/server-nest/src/modules/Roles/Roles.utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
import { ERRORS } from './constants';
|
||||
import { Role } from './models/Role.model';
|
||||
|
||||
/**
|
||||
* Valdiates role is not predefined.
|
||||
* @param {IRole} role - Role object.
|
||||
*/
|
||||
export const validateRoleNotPredefined = (role: Role) => {
|
||||
if (role.predefined) {
|
||||
throw new ServiceError(ERRORS.ROLE_PREFINED);
|
||||
}
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { events } from '@/common/events/events';
|
||||
import { CreateRoleDto } from '../dtos/Role.dto';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { validateInvalidPermissions } from '../utils';
|
||||
|
||||
@Injectable()
|
||||
export class CreateRoleService {
|
||||
@@ -25,7 +26,7 @@ export class CreateRoleService {
|
||||
*/
|
||||
public async createRole(createRoleDTO: CreateRoleDto) {
|
||||
// Validates the invalid permissions.
|
||||
this.validateInvalidPermissions(createRoleDTO.permissions);
|
||||
validateInvalidPermissions(createRoleDTO.permissions);
|
||||
|
||||
// Transformes the permissions DTO.
|
||||
const permissions = createRoleDTO.permissions;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IRoleDeletedPayload,
|
||||
} from '../Roles.types';
|
||||
import { IRoleDeletedPayload } from '../Roles.types';
|
||||
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
|
||||
import { Role } from '../models/Role.model';
|
||||
import { RolePermission } from '../models/RolePermission.model';
|
||||
@@ -10,30 +7,42 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { validateRoleNotPredefined } from '../Roles.utils';
|
||||
import { TenantUser } from '@/modules/Tenancy/TenancyModels/models/TenantUser.model';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { ERRORS } from '../constants';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteRoleService {
|
||||
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(Role.name)
|
||||
private readonly roleModel: TenantModelProxy<typeof Role>,
|
||||
|
||||
@Inject(RolePermission.name)
|
||||
private readonly rolePermissionModel: TenantModelProxy<
|
||||
typeof RolePermission
|
||||
>,
|
||||
|
||||
@Inject(TenantUser.name)
|
||||
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
|
||||
) {}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the given role from the storage.
|
||||
* @param {number} roleId - Role id.
|
||||
*/
|
||||
public async deleteRole(roleId: number): Promise<void> {
|
||||
// Retrieve the given role or throw not found serice error.
|
||||
const oldRole = await this.getRoleOrThrowError(roleId);
|
||||
// Retrieve the given role or throw not found service error.
|
||||
const oldRole = await this.roleModel()
|
||||
.query()
|
||||
.findById(roleId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate role is not predefined.
|
||||
this.validateRoleNotPredefined(oldRole);
|
||||
validateRoleNotPredefined(oldRole);
|
||||
|
||||
// Validates the given role is not associated to any user.
|
||||
await this.validateRoleNotAssociatedToUser(roleId);
|
||||
@@ -57,5 +66,18 @@ export class DeleteRoleService {
|
||||
} as IRoleDeletedPayload);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Validates the given role is not associated to any tenant users.
|
||||
* @param {number} roleId
|
||||
*/
|
||||
private validateRoleNotAssociatedToUser = async (roleId: number) => {
|
||||
const userAssociatedRole = await this.tenantUserModel()
|
||||
.query()
|
||||
.where('roleId', roleId);
|
||||
|
||||
// Throw service error if the role has associated users.
|
||||
if (userAssociatedRole.length > 0) {
|
||||
throw new ServiceError(ERRORS.CANNT_DELETE_ROLE_ASSOCIATED_TO_USERS);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,15 +6,14 @@ import { EditRoleDto } from '../dtos/Role.dto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Role } from '../models/Role.model';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { validateInvalidPermissions } from '../utils';
|
||||
|
||||
@Injectable()
|
||||
export class EditRoleService {
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(Role.name)
|
||||
private readonly roleModel: TenantModelProxy<typeof Role>,
|
||||
@@ -27,11 +26,10 @@ export class EditRoleService {
|
||||
*/
|
||||
public async editRole(roleId: number, editRoleDTO: EditRoleDto) {
|
||||
// Validates the invalid permissions.
|
||||
this.validateInvalidPermissions(editRoleDTO.permissions);
|
||||
validateInvalidPermissions(editRoleDTO.permissions);
|
||||
|
||||
// Retrieve the given role or throw not found serice error.
|
||||
const oldRole = await this.getRoleOrThrowError(roleId);
|
||||
|
||||
const oldRole = await this.roleModel().query().findById(roleId);
|
||||
const permissions = editRoleDTO.permissions;
|
||||
|
||||
// Updates the role on the storage.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
@@ -12,33 +13,62 @@ import {
|
||||
export class CommandRolePermissionDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
example: 'subject',
|
||||
description: 'The subject of the permission',
|
||||
})
|
||||
subject: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
example: 'read',
|
||||
description: 'The action of the permission',
|
||||
})
|
||||
ability: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
description: 'The value of the permission',
|
||||
})
|
||||
value: boolean;
|
||||
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: 'The permission ID',
|
||||
})
|
||||
permissionId: number;
|
||||
}
|
||||
|
||||
class CommandRoleDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
example: 'admin',
|
||||
description: 'The name of the role',
|
||||
})
|
||||
roleName: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
example: 'Administrator',
|
||||
description: 'The description of the role',
|
||||
})
|
||||
roleDescription: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CommandRolePermissionDto)
|
||||
@MinLength(1)
|
||||
@ApiProperty({
|
||||
type: [CommandRolePermissionDto],
|
||||
description: 'The permissions of the role',
|
||||
})
|
||||
permissions: Array<CommandRolePermissionDto>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import RolePermission from './RolePermission.model';
|
||||
|
||||
import { RolePermission } from './RolePermission.model';
|
||||
|
||||
export class Role extends TenantBaseModel {
|
||||
name: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
predefined: boolean;
|
||||
permissions: Array<RolePermission>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,13 +3,20 @@ import { RoleTransformer } from './RoleTransformer';
|
||||
import { Role } from '../models/Role.model';
|
||||
import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service';
|
||||
import { ServiceError } from '../../Items/ServiceError';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { CommandRolePermissionDto } from '../dtos/Role.dto';
|
||||
import { ERRORS } from '../constants';
|
||||
import { getInvalidPermissions } from '../utils';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetRoleService {
|
||||
constructor(private readonly transformer: TransformerInjectable) {}
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(Role.name)
|
||||
private readonly roleModel: TenantModelProxy<typeof Role>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the given role metadata.
|
||||
@@ -17,11 +24,11 @@ export class GetRoleService {
|
||||
* @returns {Promise<IRole>}
|
||||
*/
|
||||
public async getRole(roleId: number): Promise<Role> {
|
||||
const role = await Role.query()
|
||||
const role = await this.roleModel()
|
||||
.query()
|
||||
.findById(roleId)
|
||||
.withGraphFetched('permissions');
|
||||
|
||||
this.throwRoleNotFound(role);
|
||||
.withGraphFetched('permissions')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(role, new RoleTransformer());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { keyBy } from 'lodash';
|
||||
import { ISubjectAbilitiesSchema } from './Roles.types';
|
||||
import { CommandRolePermissionDto } from './dtos/Role.dto';
|
||||
import { AbilitySchema } from './AbilitySchema';
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
import { ERRORS } from './constants';
|
||||
|
||||
/**
|
||||
* Transformes ability schema to map.
|
||||
@@ -11,19 +15,19 @@ export function transformAbilitySchemaToMap(schema: ISubjectAbilitiesSchema[]) {
|
||||
abilities: keyBy(item.abilities, 'key'),
|
||||
extraAbilities: keyBy(item.extraAbilities, 'key'),
|
||||
})),
|
||||
'subject'
|
||||
'subject',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invalid permissions from the given defined schema.
|
||||
* @param {ISubjectAbilitiesSchema[]} schema
|
||||
* @param permissions
|
||||
* @returns
|
||||
* @param {ISubjectAbilitiesSchema[]} schema
|
||||
* @param permissions
|
||||
* @returns
|
||||
*/
|
||||
export function getInvalidPermissions(
|
||||
schema: ISubjectAbilitiesSchema[],
|
||||
permissions
|
||||
permissions,
|
||||
) {
|
||||
const schemaMap = transformAbilitySchemaToMap(schema);
|
||||
|
||||
@@ -40,3 +44,19 @@ export function getInvalidPermissions(
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the invalid given permissions.
|
||||
* @param {ICreateRolePermissionDTO[]} permissions -
|
||||
*/
|
||||
export const validateInvalidPermissions = (
|
||||
permissions: CommandRolePermissionDto[],
|
||||
) => {
|
||||
const invalidPerms = getInvalidPermissions(AbilitySchema, permissions);
|
||||
|
||||
if (invalidPerms.length > 0) {
|
||||
throw new ServiceError(ERRORS.INVALIDATE_PERMISSIONS, null, {
|
||||
invalidPermissions: invalidPerms,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -119,6 +119,6 @@ import { SaleInvoicesCost } from './SalesInvoicesCost';
|
||||
InvoicePaymentsGLEntriesRewrite,
|
||||
SaleInvoicesCost,
|
||||
],
|
||||
exports: [GetSaleInvoice, SaleInvoicesCost],
|
||||
exports: [GetSaleInvoice, SaleInvoicesCost, SaleInvoicePdf],
|
||||
})
|
||||
export class SaleInvoicesModule {}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ISaleInvoiceDeletingPayload,
|
||||
} from '../SaleInvoice.types';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TransactionPaymentServiceEntry } from '@/modules/PaymentServices/models/TransactionPaymentServiceEntry.model';
|
||||
|
||||
@Injectable()
|
||||
export class InvoicePaymentIntegrationSubscriber {
|
||||
@@ -30,7 +31,7 @@ export class InvoicePaymentIntegrationSubscriber {
|
||||
saleInvoice.paymentMethods?.filter((method) => method.enable) || [];
|
||||
|
||||
paymentMethods.map(
|
||||
async (paymentMethod: PaymentIntegrationTransactionLink) => {
|
||||
async (paymentMethod: TransactionPaymentServiceEntry) => {
|
||||
const payload = {
|
||||
...omit(paymentMethod, ['id']),
|
||||
saleInvoiceId: saleInvoice.id,
|
||||
@@ -57,7 +58,7 @@ export class InvoicePaymentIntegrationSubscriber {
|
||||
oldSaleInvoice.paymentMethods?.filter((method) => method.enable) || [];
|
||||
|
||||
paymentMethods.map(
|
||||
async (paymentMethod: PaymentIntegrationTransactionLink) => {
|
||||
async (paymentMethod: TransactionPaymentServiceEntry) => {
|
||||
const payload = {
|
||||
...omit(paymentMethod, ['id']),
|
||||
oldSaleInvoiceId: oldSaleInvoice.id,
|
||||
|
||||
@@ -11,14 +11,24 @@ import { MarkSubscriptionPaymentFailed } from './commands/MarkSubscriptionPaymen
|
||||
import { MarkSubscriptionPaymentSucceed } from './commands/MarkSubscriptionPaymentSuccessed.service';
|
||||
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
|
||||
import { PlanSubscription } from './models/PlanSubscription';
|
||||
import { MarkSubscriptionCanceled } from './commands/MarkSubscriptionCanceled.service';
|
||||
import { MarkSubscriptionPlanChanged } from './commands/MarkSubscriptionChanged.service';
|
||||
import { MarkSubscriptionResumedService } from './commands/MarkSubscriptionResumed.sevice';
|
||||
import { Plan } from './models/Plan';
|
||||
import { SubscriptionApplication } from './SubscriptionApplication';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { NewSubscriptionService } from './commands/NewSubscription.service';
|
||||
import { GetSubscriptionsService } from './queries/GetSubscriptions.service';
|
||||
import { GetLemonSqueezyCheckoutService } from './queries/GetLemonSqueezyCheckout.service';
|
||||
|
||||
|
||||
const models = [InjectSystemModel(Plan), InjectSystemModel(PlanSubscription)]
|
||||
const models = [InjectSystemModel(Plan), InjectSystemModel(PlanSubscription)];
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
...models,
|
||||
TenancyContext,
|
||||
NewSubscriptionService,
|
||||
GetSubscriptionsService,
|
||||
CancelLemonSubscription,
|
||||
ChangeLemonSubscription,
|
||||
ResumeLemonSubscription,
|
||||
@@ -26,9 +36,14 @@ const models = [InjectSystemModel(Plan), InjectSystemModel(PlanSubscription)]
|
||||
SubscribeFreeOnSignupCommunity,
|
||||
TriggerInvalidateCacheOnSubscriptionChange,
|
||||
MarkSubscriptionPaymentFailed,
|
||||
MarkSubscriptionPaymentSucceed
|
||||
MarkSubscriptionPaymentSucceed,
|
||||
MarkSubscriptionCanceled,
|
||||
MarkSubscriptionPlanChanged,
|
||||
MarkSubscriptionResumedService,
|
||||
SubscriptionApplication,
|
||||
GetLemonSqueezyCheckoutService,
|
||||
],
|
||||
controllers: [SubscriptionsController, SubscriptionsLemonWebhook],
|
||||
exports: [...models]
|
||||
exports: [...models],
|
||||
})
|
||||
export class SubscriptionModule {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Post, Req, Res } from '@nestjs/common';
|
||||
import { Controller, Post, Req } from '@nestjs/common';
|
||||
import { LemonSqueezyWebhooks } from './webhooks/LemonSqueezyWebhooks';
|
||||
|
||||
@Controller('/webhooks/lemon')
|
||||
@@ -15,6 +15,7 @@ export class SubscriptionsLemonWebhook {
|
||||
async lemonWebhooks(@Req() req: Request) {
|
||||
const data = req.body;
|
||||
const signature = (req.headers['x-signature'] as string) ?? '';
|
||||
// @ts-ignore
|
||||
const rawBody = req.rawBody;
|
||||
|
||||
await this.lemonWebhooksService.handlePostWebhook(rawBody, data, signature);
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { SubscriptionPayload } from '@/interfaces/SubscriptionPlan';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { PlanSubscription } from '../models/PlanSubscription';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { SubscriptionPayload } from '@/interfaces/SubscriptionPlan';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { Plan } from '../models/Plan';
|
||||
import { NotAllowedChangeSubscriptionPlan } from '../exceptions/NotAllowedChangeSubscriptionPlan';
|
||||
import { PlanSubscriptionRepository } from '../repositories/PlanSubscription.repository';
|
||||
|
||||
@Injectable()
|
||||
export class NewSubscriptionService {
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
|
||||
@Inject(PlanSubscription.name)
|
||||
private readonly planSubscriptionModel: typeof PlanSubscription,
|
||||
private readonly subscriptionRepository: PlanSubscriptionRepository,
|
||||
|
||||
@Inject(Plan.name)
|
||||
private readonly planModel: typeof Plan,
|
||||
@@ -56,7 +55,8 @@ export class NewSubscriptionService {
|
||||
|
||||
// No stored past tenant subscriptions create new one.
|
||||
} else {
|
||||
await tenant.newSubscription(
|
||||
await this.subscriptionRepository.newSubscription(
|
||||
tenant.id,
|
||||
plan.id,
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export class NotAllowedChangeSubscriptionPlan extends Error {
|
||||
constructor(message: string = 'Not allowed to change subscription plan.') {
|
||||
super(message);
|
||||
this.name = 'NotAllowedChangeSubscriptionPlan';
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { Model, mixin } from 'objection';
|
||||
export class Plan extends mixin(SystemModel) {
|
||||
public readonly slug: string;
|
||||
public readonly price: number;
|
||||
public readonly invoiceInternal: number;
|
||||
public readonly invoicePeriod: string;
|
||||
public readonly invoiceInternal: 'month' | 'year';
|
||||
public readonly invoicePeriod: number;
|
||||
public readonly trialPeriod: string;
|
||||
public readonly trialInterval: number;
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ import { SystemModel } from '@/modules/System/models/SystemModel';
|
||||
import { SubscriptionPaymentStatus } from '@/interfaces/SubscriptionPlan';
|
||||
|
||||
export class PlanSubscription extends mixin(SystemModel) {
|
||||
public readonly lemonSubscriptionId: number;
|
||||
public readonly endsAt: Date;
|
||||
public readonly startsAt: Date;
|
||||
public readonly canceledAt: Date;
|
||||
public readonly trialEndsAt: Date;
|
||||
public readonly paymentStatus: SubscriptionPaymentStatus;
|
||||
public readonly planId: number;
|
||||
public readonly lemonSubscriptionId!: string;
|
||||
public readonly slug: string;
|
||||
public readonly endsAt!: Date;
|
||||
public readonly startsAt!: Date;
|
||||
public readonly canceledAt!: Date;
|
||||
public readonly trialEndsAt!: Date;
|
||||
public readonly paymentStatus!: SubscriptionPaymentStatus;
|
||||
public readonly tenantId!: number;
|
||||
public readonly planId!: number;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { TenantRepository } from '@/common/repository/TenantRepository';
|
||||
import { SubscriptionPeriod } from '../SubscriptionPeriod';
|
||||
import { PlanSubscription } from '../models/PlanSubscription';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SystemKnexConnection } from '@/modules/System/SystemDB/SystemDB.constants';
|
||||
import { Knex } from 'knex';
|
||||
import { PartialModelObject } from 'objection';
|
||||
|
||||
@Injectable()
|
||||
export class PlanSubscriptionRepository extends TenantRepository {
|
||||
constructor(
|
||||
@Inject(SystemKnexConnection)
|
||||
private readonly tenantDBKnex: Knex,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the repository's model.
|
||||
*/
|
||||
get model(): typeof PlanSubscription {
|
||||
return PlanSubscription.bindKnex(this.tenantDBKnex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a new subscription for the associated tenant.
|
||||
*/
|
||||
newSubscription(
|
||||
tenantId: number,
|
||||
planId: number,
|
||||
invoiceInterval: 'month' | 'year',
|
||||
invoicePeriod: number,
|
||||
subscriptionSlug: string,
|
||||
payload?: { lemonSqueezyId?: string },
|
||||
) {
|
||||
const period = new SubscriptionPeriod(invoiceInterval, invoicePeriod);
|
||||
const model: PartialModelObject<PlanSubscription> = {
|
||||
tenantId,
|
||||
slug: subscriptionSlug,
|
||||
planId,
|
||||
startsAt: period.getStartDate(),
|
||||
endsAt: period.getEndDate(),
|
||||
lemonSubscriptionId: payload?.lemonSqueezyId || null,
|
||||
};
|
||||
return this.model.query().insert({ ...model });
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export const ERRORS = {
|
||||
};
|
||||
|
||||
export interface IOrganizationSubscriptionChanged {
|
||||
lemonSubscriptionId: number;
|
||||
lemonSubscriptionId: string;
|
||||
newVariantId: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,63 @@
|
||||
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: boolean;
|
||||
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('./Subscriptions/PlanSubscription');
|
||||
const {
|
||||
PlanSubscription,
|
||||
} = require('../../Subscription/models/PlanSubscription');
|
||||
const { TenantMetadata } = require('./TenantMetadataModel');
|
||||
|
||||
return {
|
||||
@@ -32,6 +69,15 @@ export class TenantModel extends BaseModel {
|
||||
to: 'tenants_metadata.tenantId',
|
||||
},
|
||||
},
|
||||
|
||||
subscriptions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: PlanSubscription,
|
||||
join: {
|
||||
from: 'tenants.id',
|
||||
to: 'subscription_plan_subscriptions.tenantId',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import uniqid from 'uniqid';
|
||||
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(
|
||||
@Inject(SystemKnexConnection)
|
||||
private readonly tenantDBKnex: Knex,
|
||||
|
||||
@Inject(TenantMetadata.name)
|
||||
private readonly tenantMetadataModel: typeof TenantMetadata,
|
||||
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
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'),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TenantsManagerService } from './TenantsManager';
|
||||
import { TenantDBManager } from './TenantDBManager';
|
||||
|
||||
@Module({})
|
||||
export class TenantDBManagerModule {}
|
||||
export class TenantDBManagerModule {
|
||||
providers: [TenantsManagerService, TenantDBManager];
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import { TenantDBAlreadyExists } from './exceptions/TenantDBAlreadyExists';
|
||||
import { sanitizeDatabaseName } from '@/utils/sanitize-database-name';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SystemKnexConnection } from '../System/SystemDB/SystemDB.constants';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModel } from '../System/models/TenantModel';
|
||||
|
||||
@Injectable()
|
||||
export class TenantDBManager {
|
||||
static knexCache: { [key: string]: Knex } = {};
|
||||
|
||||
@@ -17,20 +19,20 @@ export class TenantDBManager {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the tenant database name.
|
||||
* Retrieves the tenant database name.
|
||||
* @return {string}
|
||||
*/
|
||||
private getDatabaseName(tenant: ITenant) {
|
||||
private getDatabaseName(tenant: TenantModel) {
|
||||
return sanitizeDatabaseName(
|
||||
`${this.configService.get('tenant.db_name_prefix')}${tenant.organizationId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the tenant database weather exists.
|
||||
* Determines the tenant database weather exists.
|
||||
* @return {Promise<boolean>}
|
||||
*/
|
||||
public async databaseExists(tenant: ITenant) {
|
||||
public async databaseExists(tenant: TenantModel) {
|
||||
const databaseName = this.getDatabaseName(tenant);
|
||||
const results = await this.systemKnex.raw(
|
||||
'SELECT * FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "' +
|
||||
@@ -45,7 +47,7 @@ export class TenantDBManager {
|
||||
* @throws {TenantAlreadyInitialized}
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async createDatabase(tenant: ITenant): Promise<void> {
|
||||
public async createDatabase(tenant: TenantModel): Promise<void> {
|
||||
await this.throwErrorIfTenantDBExists(tenant);
|
||||
|
||||
const databaseName = this.getDatabaseName(tenant);
|
||||
@@ -58,7 +60,7 @@ export class TenantDBManager {
|
||||
* Dropdowns the tenant database if it was exist.
|
||||
* @param {ITenant} tenant -
|
||||
*/
|
||||
public async dropDatabaseIfExists(tenant: ITenant) {
|
||||
public async dropDatabaseIfExists(tenant: TenantModel) {
|
||||
const isExists = await this.databaseExists(tenant);
|
||||
|
||||
if (!isExists) {
|
||||
@@ -71,7 +73,7 @@ export class TenantDBManager {
|
||||
* dropdowns the tenant's database.
|
||||
* @param {ITenant} tenant
|
||||
*/
|
||||
public async dropDatabase(tenant: ITenant) {
|
||||
public async dropDatabase(tenant: TenantModel) {
|
||||
const databaseName = this.getDatabaseName(tenant);
|
||||
|
||||
await this.systemKnex.raw(`DROP DATABASE IF EXISTS ${databaseName}`);
|
||||
@@ -81,7 +83,7 @@ export class TenantDBManager {
|
||||
* Migrate tenant database schema to the latest version.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async migrate(tenant: ITenant): Promise<void> {
|
||||
public async migrate(tenant: TenantModel): Promise<void> {
|
||||
const knex = this.setupKnexInstance(tenant);
|
||||
await knex.migrate.latest();
|
||||
}
|
||||
@@ -90,7 +92,7 @@ export class TenantDBManager {
|
||||
* Seeds initial data to the tenant database.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async seed(tenant: ITenant): Promise<void> {
|
||||
public async seed(tenant: TenantModel): Promise<void> {
|
||||
const knex = this.setupKnexInstance(tenant);
|
||||
|
||||
await knex.migrate.latest({
|
||||
@@ -103,7 +105,7 @@ export class TenantDBManager {
|
||||
* Retrieve the knex instance of tenant.
|
||||
* @return {Knex}
|
||||
*/
|
||||
private setupKnexInstance(tenant: ITenant) {
|
||||
private setupKnexInstance(tenant: TenantModel) {
|
||||
const key: string = `${tenant.id}`;
|
||||
let knexInstance = TenantDBManager.knexCache[key];
|
||||
|
||||
@@ -134,7 +136,7 @@ export class TenantDBManager {
|
||||
* Throws error if the tenant database already exists.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async throwErrorIfTenantDBExists(tenant: ITenant) {
|
||||
async throwErrorIfTenantDBExists(tenant: TenantModel) {
|
||||
const isExists = await this.databaseExists(tenant);
|
||||
if (isExists) {
|
||||
throw new TenantDBAlreadyExists();
|
||||
|
||||
@@ -1,49 +1,30 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { TenantDBManager } from "./TenantDBManager";
|
||||
import { EventEmitter2 } from "@nestjs/event-emitter";
|
||||
import { events } from "@/common/events/events";
|
||||
|
||||
// import { Container, Inject, Service } from 'typedi';
|
||||
// import { ITenantManager, ITenant, ITenantDBManager } from '@/interfaces';
|
||||
// import {
|
||||
// EventDispatcherInterface,
|
||||
// EventDispatcher,
|
||||
// } from 'decorators/eventDispatcher';
|
||||
// import {
|
||||
// TenantAlreadyInitialized,
|
||||
// TenantAlreadySeeded,
|
||||
// TenantDatabaseNotBuilt,
|
||||
// } from '@/exceptions';
|
||||
// import TenantDBManager from '@/services/Tenancy/TenantDBManager';
|
||||
// import events from '@/subscribers/events';
|
||||
// import { Tenant } from '@/system/models';
|
||||
// import { SeedMigration } from '@/lib/Seeder/SeedMigration';
|
||||
// import i18n from '../../loaders/i18n';
|
||||
|
||||
// const ERRORS = {
|
||||
// TENANT_ALREADY_CREATED: 'TENANT_ALREADY_CREATED',
|
||||
// TENANT_NOT_EXISTS: 'TENANT_NOT_EXISTS',
|
||||
// };
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TenantDBManager } from './TenantDBManager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModel } from '../System/models/TenantModel';
|
||||
import {
|
||||
throwErrorIfTenantAlreadyInitialized,
|
||||
throwErrorIfTenantAlreadySeeded,
|
||||
throwErrorIfTenantNotBuilt,
|
||||
} from './_utils';
|
||||
import { SeedMigration } from '@/libs/migration-seed/SeedMigration';
|
||||
import { TenantRepository } from '../System/repositories/Tenant.repository';
|
||||
|
||||
@Injectable()
|
||||
export class TenantsManagerService {
|
||||
|
||||
constructor(
|
||||
private readonly tenantDbManager: TenantDBManager,
|
||||
private readonly eventEmitter: EventEmitter2
|
||||
) {
|
||||
}
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly tenantRepository: TenantRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new teant with unique organization id.
|
||||
* @param {ITenant} tenant
|
||||
* @return {Promise<ITenant>}
|
||||
* @return {Promise<TenantModel>}
|
||||
*/
|
||||
public async createTenant(): Promise<ITenant> {
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
const tenant = await tenantRepository.createWithUniqueOrgId();
|
||||
|
||||
return tenant;
|
||||
public async createTenant(): Promise<TenantModel> {
|
||||
return this.tenantRepository.createWithUniqueOrgId();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,8 +32,8 @@ export class TenantsManagerService {
|
||||
* @param {ITenant} tenant -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async createDatabase(tenant: ITenant): Promise<void> {
|
||||
this.throwErrorIfTenantAlreadyInitialized(tenant);
|
||||
public async createDatabase(tenant: TenantModel): Promise<void> {
|
||||
throwErrorIfTenantAlreadyInitialized(tenant);
|
||||
|
||||
await this.tenantDbManager.createDatabase(tenant);
|
||||
|
||||
@@ -63,17 +44,17 @@ export class TenantsManagerService {
|
||||
* Drops the database if the given tenant.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
async dropDatabaseIfExists(tenant: ITenant) {
|
||||
async dropDatabaseIfExists(tenant: TenantModel) {
|
||||
// Drop the database if exists.
|
||||
await this.tenantDbManager.dropDatabaseIfExists(tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the tenant has database.
|
||||
* Determines the tenant has database.
|
||||
* @param {ITenant} tenant
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public async hasDatabase(tenant: ITenant): Promise<boolean> {
|
||||
public async hasDatabase(tenant: TenantModel): Promise<boolean> {
|
||||
return this.tenantDbManager.databaseExists(tenant);
|
||||
}
|
||||
|
||||
@@ -82,18 +63,18 @@ export class TenantsManagerService {
|
||||
* @param {ITenant} tenant
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async migrateTenant(tenant: ITenant): Promise<void> {
|
||||
public async migrateTenant(tenant: TenantModel): Promise<void> {
|
||||
// Throw error if the tenant already initialized.
|
||||
this.throwErrorIfTenantAlreadyInitialized(tenant);
|
||||
throwErrorIfTenantAlreadyInitialized(tenant);
|
||||
|
||||
// Migrate the database tenant.
|
||||
await this.tenantDbManager.migrate(tenant);
|
||||
|
||||
// Mark the tenant as initialized.
|
||||
await Tenant.markAsInitialized(tenant.id);
|
||||
await this.tenantRepository.markAsInitialized().findById(tenant.id);
|
||||
|
||||
// Triggers `onTenantMigrated` event.
|
||||
this.eventDispatcher.dispatch(events.tenantManager.tenantMigrated, {
|
||||
this.eventEmitter.emitAsync(events.tenantManager.tenantMigrated, {
|
||||
tenantId: tenant.id,
|
||||
});
|
||||
}
|
||||
@@ -103,21 +84,21 @@ export class TenantsManagerService {
|
||||
* @param {ITenant} tenant
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async seedTenant(tenant: ITenant, tenancyContext): Promise<void> {
|
||||
public async seedTenant(tenant: TenantModel, tenancyContext): Promise<void> {
|
||||
// Throw error if the tenant is not built yet.
|
||||
this.throwErrorIfTenantNotBuilt(tenant);
|
||||
throwErrorIfTenantNotBuilt(tenant);
|
||||
|
||||
// Throw error if the tenant is not seeded yet.
|
||||
this.throwErrorIfTenantAlreadySeeded(tenant);
|
||||
throwErrorIfTenantAlreadySeeded(tenant);
|
||||
|
||||
// Seeds the organization database data.
|
||||
await new SeedMigration(tenancyContext.knex, tenancyContext).latest();
|
||||
|
||||
// Mark the tenant as seeded in specific date.
|
||||
await Tenant.markAsSeeded(tenant.id);
|
||||
await this.tenantRepository.markAsSeeded().findById(tenant.id);
|
||||
|
||||
// Triggers `onTenantSeeded` event.
|
||||
this.eventDispatcher.dispatch(events.tenantManager.tenantSeeded, {
|
||||
this.eventEmitter.emitAsync(events.tenantManager.tenantSeeded, {
|
||||
tenantId: tenant.id,
|
||||
});
|
||||
}
|
||||
@@ -127,8 +108,8 @@ export class TenantsManagerService {
|
||||
* @param {ITenant} tenant
|
||||
* @returns {Knex}
|
||||
*/
|
||||
public setupKnexInstance(tenant: ITenant) {
|
||||
return this.tenantDBManager.setupKnexInstance(tenant);
|
||||
public setupKnexInstance(tenant: TenantModel) {
|
||||
// return this.tenantDbManager.setupKnexInstance(tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,55 +118,6 @@ export class TenantsManagerService {
|
||||
* @returns {Knex}
|
||||
*/
|
||||
public getKnexInstance(tenantId: number) {
|
||||
return this.tenantDBManager.getKnexInstance(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the tenant already seeded.
|
||||
* @throws {TenantAlreadySeeded}
|
||||
*/
|
||||
private throwErrorIfTenantAlreadySeeded(tenant: ITenant) {
|
||||
if (tenant.seededAt) {
|
||||
throw new TenantAlreadySeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the tenant database is not built yut.
|
||||
* @param {ITenant} tenant
|
||||
*/
|
||||
private throwErrorIfTenantNotBuilt(tenant: ITenant) {
|
||||
if (!tenant.initializedAt) {
|
||||
throw new TenantDatabaseNotBuilt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the tenant already migrated.
|
||||
* @throws {TenantAlreadyInitialized}
|
||||
*/
|
||||
private throwErrorIfTenantAlreadyInitialized(tenant: ITenant) {
|
||||
if (tenant.initializedAt) {
|
||||
throw new TenantAlreadyInitialized();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize seed migration contxt.
|
||||
* @param {ITenant} tenant
|
||||
* @returns
|
||||
*/
|
||||
public getSeedMigrationContext(tenant: ITenant) {
|
||||
// Initialize the knex instance.
|
||||
const knex = this.setupKnexInstance(tenant);
|
||||
const i18nInstance = i18n();
|
||||
|
||||
i18nInstance.setLocale(tenant.metadata.language);
|
||||
|
||||
return {
|
||||
knex,
|
||||
i18n: i18nInstance,
|
||||
tenant,
|
||||
};
|
||||
return this.tenantDbManager.getKnexInstance(tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
34
packages/server-nest/src/modules/TenantDBManager/_utils.ts
Normal file
34
packages/server-nest/src/modules/TenantDBManager/_utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { TenantModel } from '../System/models/TenantModel';
|
||||
import { TenantAlreadyInitialized } from './exceptions/TenantAlreadyInitialized';
|
||||
import { TenantAlreadySeeded } from './exceptions/TenantAlreadySeeded';
|
||||
import { TenantDatabaseNotBuilt } from './exceptions/TenantDatabaseNotBuilt';
|
||||
|
||||
/**
|
||||
* Throws error if the tenant already seeded.
|
||||
* @throws {TenantAlreadySeeded}
|
||||
*/
|
||||
export const throwErrorIfTenantAlreadySeeded = (tenant: TenantModel) => {
|
||||
if (tenant.seededAt) {
|
||||
throw new TenantAlreadySeeded();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws error if the tenant database is not built yut.
|
||||
* @param {ITenant} tenant
|
||||
*/
|
||||
export const throwErrorIfTenantNotBuilt = (tenant: TenantModel) => {
|
||||
if (!tenant.initializedAt) {
|
||||
throw new TenantDatabaseNotBuilt();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws error if the tenant already migrated.
|
||||
* @throws {TenantAlreadyInitialized}
|
||||
*/
|
||||
export const throwErrorIfTenantAlreadyInitialized = (tenant: TenantModel) => {
|
||||
if (tenant.initializedAt) {
|
||||
throw new TenantAlreadyInitialized();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export class TenantAlreadyInitialized extends Error {
|
||||
constructor(description: string = 'Tenant is already initialized') {
|
||||
super(description);
|
||||
this.name = 'TenantAlreadyInitialized';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class TenantAlreadySeeded extends Error {
|
||||
constructor(description: string = 'Tenant is already seeded') {
|
||||
super(description);
|
||||
this.name = 'TenantAlreadySeeded';
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
export class TenantDBAlreadyExists {
|
||||
constructor() {
|
||||
|
||||
export class TenantDBAlreadyExists extends Error {
|
||||
constructor(description: string = 'Tenant DB is already exists.') {
|
||||
super(description);
|
||||
this.name = 'TenantDBAlreadyExists';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export class TenantDatabaseNotBuilt extends Error {
|
||||
constructor(description: string = 'Tenant database is not built yet.') {
|
||||
super(description);
|
||||
this.name = 'TenantDatabaseNotBuilt';
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,9 @@
|
||||
"./src/modules/FinancialStatements/modules/BalanceSheet/**.ts",
|
||||
"./src/modules/FinancialStatements/modules/ProfitLossSheet/**.ts",
|
||||
"./src/modules/Views",
|
||||
"./src/modules/TenantDBManager",
|
||||
"./src/modules/TenantDBManager/**.ts",
|
||||
"./src/modules/Organization",
|
||||
"./src/modules/Organization/**.ts"
|
||||
]
|
||||
}
|
||||
|
||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
@@ -484,6 +484,12 @@ importers:
|
||||
|
||||
packages/server-nest:
|
||||
dependencies:
|
||||
'@aws-sdk/client-s3':
|
||||
specifier: ^3.576.0
|
||||
version: 3.583.0
|
||||
'@aws-sdk/s3-request-presigner':
|
||||
specifier: ^3.583.0
|
||||
version: 3.583.0
|
||||
'@bigcapital/email-components':
|
||||
specifier: '*'
|
||||
version: link:../../shared/email-components
|
||||
@@ -496,6 +502,9 @@ importers:
|
||||
'@bigcapital/utils':
|
||||
specifier: '*'
|
||||
version: link:../../shared/bigcapital-utils
|
||||
'@casl/ability':
|
||||
specifier: ^5.4.3
|
||||
version: 5.4.4
|
||||
'@lemonsqueezy/lemonsqueezy.js':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
@@ -613,12 +622,21 @@ importers:
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
lru-cache:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
mathjs:
|
||||
specifier: ^9.4.0
|
||||
version: 9.5.2
|
||||
moment:
|
||||
specifier: ^2.30.1
|
||||
version: 2.30.1
|
||||
moment-range:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2(moment@2.30.1)
|
||||
moment-timezone:
|
||||
specifier: ^0.5.43
|
||||
version: 0.5.45
|
||||
mysql:
|
||||
specifier: ^2.18.1
|
||||
version: 2.18.1
|
||||
@@ -1516,7 +1534,7 @@ packages:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.583.0(@aws-sdk/client-sts@3.583.0)
|
||||
'@aws-sdk/client-sts': 3.583.0
|
||||
'@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)
|
||||
'@aws-sdk/core': 3.582.0
|
||||
'@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0)
|
||||
'@aws-sdk/middleware-host-header': 3.577.0
|
||||
@@ -1567,7 +1585,7 @@ packages:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.583.0(@aws-sdk/client-sts@3.583.0)
|
||||
'@aws-sdk/client-sts': 3.583.0
|
||||
'@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)
|
||||
'@aws-sdk/core': 3.582.0
|
||||
'@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0)
|
||||
'@aws-sdk/middleware-bucket-endpoint': 3.577.0
|
||||
@@ -1631,7 +1649,7 @@ packages:
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sts': 3.583.0
|
||||
'@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)
|
||||
'@aws-sdk/core': 3.582.0
|
||||
'@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0)
|
||||
'@aws-sdk/middleware-host-header': 3.577.0
|
||||
@@ -1720,7 +1738,7 @@ packages:
|
||||
- aws-crt
|
||||
dev: false
|
||||
|
||||
/@aws-sdk/client-sts@3.583.0:
|
||||
/@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0):
|
||||
resolution: {integrity: sha512-xDMxiemPDWr9dY2Q4AyixkRnk/hvS6fs6OWxuVCz1WO47YhaAfOsEGAgQMgDLLaOfj/oLU5D14uTNBEPGh4rBA==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
dependencies:
|
||||
@@ -1765,6 +1783,7 @@ packages:
|
||||
'@smithy/util-utf8': 3.0.0
|
||||
tslib: 2.8.0
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/client-sso-oidc'
|
||||
- aws-crt
|
||||
dev: false
|
||||
|
||||
@@ -1827,7 +1846,7 @@ packages:
|
||||
peerDependencies:
|
||||
'@aws-sdk/client-sts': ^3.583.0
|
||||
dependencies:
|
||||
'@aws-sdk/client-sts': 3.583.0
|
||||
'@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)
|
||||
'@aws-sdk/credential-provider-env': 3.577.0
|
||||
'@aws-sdk/credential-provider-process': 3.577.0
|
||||
'@aws-sdk/credential-provider-sso': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)
|
||||
@@ -1898,7 +1917,7 @@ packages:
|
||||
peerDependencies:
|
||||
'@aws-sdk/client-sts': ^3.577.0
|
||||
dependencies:
|
||||
'@aws-sdk/client-sts': 3.583.0
|
||||
'@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)
|
||||
'@aws-sdk/types': 3.577.0
|
||||
'@smithy/property-provider': 3.0.0
|
||||
'@smithy/types': 3.0.0
|
||||
@@ -1912,7 +1931,7 @@ packages:
|
||||
dependencies:
|
||||
'@aws-sdk/client-cognito-identity': 3.583.0
|
||||
'@aws-sdk/client-sso': 3.583.0
|
||||
'@aws-sdk/client-sts': 3.583.0
|
||||
'@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)
|
||||
'@aws-sdk/credential-provider-cognito-identity': 3.583.0
|
||||
'@aws-sdk/credential-provider-env': 3.577.0
|
||||
'@aws-sdk/credential-provider-http': 3.582.0
|
||||
|
||||
Reference in New Issue
Block a user