From 6461a2318f84977db8a397f0b690ff6c2870171f Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 27 Mar 2025 23:13:17 +0200 Subject: [PATCH] refactor: implement tenant database management and seeding utilities --- packages/server-nest/package.json | 4 + .../src/libs/migration-seed/FsMigrations.ts | 100 ++++++++ .../src/libs/migration-seed/MigrateUtils.ts | 193 +++++++++++++++ .../src/libs/migration-seed/SeedMigration.ts | 223 ++++++++++++++++++ .../src/libs/migration-seed/Seeder.ts | 11 + .../src/libs/migration-seed/SeederConfig.ts | 44 ++++ .../src/libs/migration-seed/TableUtils.ts | 43 ++++ .../src/libs/migration-seed/TenantSeeder.ts | 25 ++ .../src/libs/migration-seed/Utils.ts | 43 ++++ .../src/libs/migration-seed/constants.ts | 12 + .../src/libs/migration-seed/interfaces.ts | 20 ++ .../server-nest/src/modules/App/App.module.ts | 2 +- .../src/modules/Dashboard/Dashboard.module.ts | 3 +- .../commands/EditManualJournal.service.ts | 1 - .../Organization/Organization.module.ts | 4 +- .../Organization/Organization.types.ts | 8 +- .../Organization/Organization.utils.ts | 5 +- .../commands/BuildOrganization.service.ts | 93 ++++---- .../CommandOrganizationValidators.service.ts | 42 ++++ .../commands/UpdateOrganization.service.ts | 37 +-- .../Organization/dtos/Organization.dto.ts | 93 +++++++- .../GetInvoicePaymentLinkMetadata.ts | 15 +- .../PaymentLinks/GetPaymentLinkInvoicePdf.ts | 17 +- .../PaymentLinks/PaymentLinks.module.ts | 7 +- .../src/modules/Roles/Roles.utils.ts | 13 + .../Roles/commands/CreateRole.service.ts | 3 +- .../Roles/commands/DeleteRole.service.ts | 40 +++- .../Roles/commands/EditRole.service.ts | 8 +- .../src/modules/Roles/dtos/Role.dto.ts | 30 +++ .../src/modules/Roles/models/Role.model.ts | 4 +- .../modules/Roles/queries/GetRole.service.ts | 19 +- .../server-nest/src/modules/Roles/utils.ts | 30 ++- .../SaleInvoices/SaleInvoices.module.ts | 2 +- .../InvoicePaymentIntegrationSubscriber.ts | 5 +- .../Subscription/Subscription.module.ts | 23 +- .../SubscriptionsLemonWebhook.controller.ts | 3 +- .../commands/NewSubscription.service.ts | 14 +- .../NotAllowedChangeSubscriptionPlan.ts | 6 + .../src/modules/Subscription/models/Plan.ts | 4 +- .../Subscription/models/PlanSubscription.ts | 16 +- .../PlanSubscription.repository.ts | 47 ++++ .../src/modules/Subscription/types.ts | 2 +- .../src/modules/System/models/TenantModel.ts | 54 ++++- .../System/repositories/Tenant.repository.ts | 132 +++++++++++ .../TenantDBManager/TenantDBManager.module.ts | 6 +- .../TenantDBManager/TenantDBManager.ts | 26 +- .../modules/TenantDBManager/TenantsManager.ts | 138 +++-------- .../src/modules/TenantDBManager/_utils.ts | 34 +++ .../exceptions/TenantAlreadyInitialized.ts | 6 + .../exceptions/TenantAlreadySeeded.ts | 6 + .../exceptions/TenantDBAlreadyExists.ts | 10 +- .../exceptions/TenantDatabaseNotBuilt.ts | 6 + packages/server-nest/tsconfig.build.json | 4 + pnpm-lock.yaml | 33 ++- 54 files changed, 1497 insertions(+), 272 deletions(-) create mode 100644 packages/server-nest/src/libs/migration-seed/FsMigrations.ts create mode 100644 packages/server-nest/src/libs/migration-seed/MigrateUtils.ts create mode 100644 packages/server-nest/src/libs/migration-seed/SeedMigration.ts create mode 100644 packages/server-nest/src/libs/migration-seed/Seeder.ts create mode 100644 packages/server-nest/src/libs/migration-seed/SeederConfig.ts create mode 100644 packages/server-nest/src/libs/migration-seed/TableUtils.ts create mode 100644 packages/server-nest/src/libs/migration-seed/TenantSeeder.ts create mode 100644 packages/server-nest/src/libs/migration-seed/Utils.ts create mode 100644 packages/server-nest/src/libs/migration-seed/constants.ts create mode 100644 packages/server-nest/src/libs/migration-seed/interfaces.ts create mode 100644 packages/server-nest/src/modules/Organization/commands/CommandOrganizationValidators.service.ts create mode 100644 packages/server-nest/src/modules/Roles/Roles.utils.ts create mode 100644 packages/server-nest/src/modules/Subscription/exceptions/NotAllowedChangeSubscriptionPlan.ts create mode 100644 packages/server-nest/src/modules/Subscription/repositories/PlanSubscription.repository.ts create mode 100644 packages/server-nest/src/modules/System/repositories/Tenant.repository.ts create mode 100644 packages/server-nest/src/modules/TenantDBManager/_utils.ts create mode 100644 packages/server-nest/src/modules/TenantDBManager/exceptions/TenantAlreadyInitialized.ts create mode 100644 packages/server-nest/src/modules/TenantDBManager/exceptions/TenantAlreadySeeded.ts create mode 100644 packages/server-nest/src/modules/TenantDBManager/exceptions/TenantDatabaseNotBuilt.ts diff --git a/packages/server-nest/package.json b/packages/server-nest/package.json index 1c1a8243b..925f30fc1 100644 --- a/packages/server-nest/package.json +++ b/packages/server-nest/package.json @@ -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", diff --git a/packages/server-nest/src/libs/migration-seed/FsMigrations.ts b/packages/server-nest/src/libs/migration-seed/FsMigrations.ts new file mode 100644 index 000000000..605f6b421 --- /dev/null +++ b/packages/server-nest/src/libs/migration-seed/FsMigrations.ts @@ -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 + */ + public getMigrations(loadExtensions = null): Promise { + // 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 }; diff --git a/packages/server-nest/src/libs/migration-seed/MigrateUtils.ts b/packages/server-nest/src/libs/migration-seed/MigrateUtils.ts new file mode 100644 index 000000000..35feab077 --- /dev/null +++ b/packages/server-nest/src/libs/migration-seed/MigrateUtils.ts @@ -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 { + 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 + */ +export async function listCompleted( + tableName: string, + schemaName: string, + trxOrKnex, +): Promise { + 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) + ); + }); +} diff --git a/packages/server-nest/src/libs/migration-seed/SeedMigration.ts b/packages/server-nest/src/libs/migration-seed/SeedMigration.ts new file mode 100644 index 000000000..d87fc10be --- /dev/null +++ b/packages/server-nest/src/libs/migration-seed/SeedMigration.ts @@ -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} + */ + async latest(config = null): Promise { + // 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 { + 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} + */ + private waterfallBatch( + batchNo: number, + migrations: MigrateItem[], + direction: string, + trx: Knex.Transaction, + ): Promise { + 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} + */ + private async runBatch( + migrations: MigrateItem[], + direction: string, + trx: Knex.Transaction, + ): Promise { + // 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); + } +} diff --git a/packages/server-nest/src/libs/migration-seed/Seeder.ts b/packages/server-nest/src/libs/migration-seed/Seeder.ts new file mode 100644 index 000000000..8ad674048 --- /dev/null +++ b/packages/server-nest/src/libs/migration-seed/Seeder.ts @@ -0,0 +1,11 @@ + +export class Seeder { + knex: any; + + constructor(knex) { + this.knex = knex; + } + up(knex) {} + down(knex) {} +} + diff --git a/packages/server-nest/src/libs/migration-seed/SeederConfig.ts b/packages/server-nest/src/libs/migration-seed/SeederConfig.ts new file mode 100644 index 000000000..77ea2e57d --- /dev/null +++ b/packages/server-nest/src/libs/migration-seed/SeederConfig.ts @@ -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; +} diff --git a/packages/server-nest/src/libs/migration-seed/TableUtils.ts b/packages/server-nest/src/libs/migration-seed/TableUtils.ts new file mode 100644 index 000000000..587112887 --- /dev/null +++ b/packages/server-nest/src/libs/migration-seed/TableUtils.ts @@ -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); +} diff --git a/packages/server-nest/src/libs/migration-seed/TenantSeeder.ts b/packages/server-nest/src/libs/migration-seed/TenantSeeder.ts new file mode 100644 index 000000000..6c54b868e --- /dev/null +++ b/packages/server-nest/src/libs/migration-seed/TenantSeeder.ts @@ -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; + } +} diff --git a/packages/server-nest/src/libs/migration-seed/Utils.ts b/packages/server-nest/src/libs/migration-seed/Utils.ts new file mode 100644 index 000000000..3a6be0009 --- /dev/null +++ b/packages/server-nest/src/libs/migration-seed/Utils.ts @@ -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}`); +} diff --git a/packages/server-nest/src/libs/migration-seed/constants.ts b/packages/server-nest/src/libs/migration-seed/constants.ts new file mode 100644 index 000000000..86fa35eec --- /dev/null +++ b/packages/server-nest/src/libs/migration-seed/constants.ts @@ -0,0 +1,12 @@ +// Default load extensions. +export const DEFAULT_LOAD_EXTENSIONS = [ + '.co', + '.coffee', + '.eg', + '.iced', + '.js', + '.cjs', + '.litcoffee', + '.ls', + '.ts', +]; diff --git a/packages/server-nest/src/libs/migration-seed/interfaces.ts b/packages/server-nest/src/libs/migration-seed/interfaces.ts new file mode 100644 index 000000000..57550a5f1 --- /dev/null +++ b/packages/server-nest/src/libs/migration-seed/interfaces.ts @@ -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; +} diff --git a/packages/server-nest/src/modules/App/App.module.ts b/packages/server-nest/src/modules/App/App.module.ts index 303cd83a0..91ddd24f5 100644 --- a/packages/server-nest/src/modules/App/App.module.ts +++ b/packages/server-nest/src/modules/App/App.module.ts @@ -190,7 +190,7 @@ import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module RolesModule, SubscriptionModule, OrganizationModule, - TenantDBManagerModule + TenantDBManagerModule, ], controllers: [AppController], providers: [ diff --git a/packages/server-nest/src/modules/Dashboard/Dashboard.module.ts b/packages/server-nest/src/modules/Dashboard/Dashboard.module.ts index b5f646797..1a52e2596 100644 --- a/packages/server-nest/src/modules/Dashboard/Dashboard.module.ts +++ b/packages/server-nest/src/modules/Dashboard/Dashboard.module.ts @@ -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 {} diff --git a/packages/server-nest/src/modules/ManualJournals/commands/EditManualJournal.service.ts b/packages/server-nest/src/modules/ManualJournals/commands/EditManualJournal.service.ts index e652ae402..11df0fde4 100644 --- a/packages/server-nest/src/modules/ManualJournals/commands/EditManualJournal.service.ts +++ b/packages/server-nest/src/modules/ManualJournals/commands/EditManualJournal.service.ts @@ -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'; diff --git a/packages/server-nest/src/modules/Organization/Organization.module.ts b/packages/server-nest/src/modules/Organization/Organization.module.ts index aab055ee1..3b0bcb587 100644 --- a/packages/server-nest/src/modules/Organization/Organization.module.ts +++ b/packages/server-nest/src/modules/Organization/Organization.module.ts @@ -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], diff --git a/packages/server-nest/src/modules/Organization/Organization.types.ts b/packages/server-nest/src/modules/Organization/Organization.types.ts index 718b75643..762fd0fe3 100644 --- a/packages/server-nest/src/modules/Organization/Organization.types.ts +++ b/packages/server-nest/src/modules/Organization/Organization.types.ts @@ -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; -} \ No newline at end of file +export interface OrganizationBuildQueueJobPayload extends TenantJobPayload { + buildDto: BuildOrganizationDto; +} diff --git a/packages/server-nest/src/modules/Organization/Organization.utils.ts b/packages/server-nest/src/modules/Organization/Organization.utils.ts index 9273bf0b1..780318a08 100644 --- a/packages/server-nest/src/modules/Organization/Organization.utils.ts +++ b/packages/server-nest/src/modules/Organization/Organization.utils.ts @@ -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'), diff --git a/packages/server-nest/src/modules/Organization/commands/BuildOrganization.service.ts b/packages/server-nest/src/modules/Organization/commands/BuildOrganization.service.ts index 11c14d88d..baa00ee06 100644 --- a/packages/server-nest/src/modules/Organization/commands/BuildOrganization.service.ts +++ b/packages/server-nest/src/modules/Organization/commands/BuildOrganization.service.ts @@ -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} */ - public async build( - buildDTO: BuildOrganizationDto, - ): Promise { + public async build(buildDTO: BuildOrganizationDto): Promise { 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 }); - } } diff --git a/packages/server-nest/src/modules/Organization/commands/CommandOrganizationValidators.service.ts b/packages/server-nest/src/modules/Organization/commands/CommandOrganizationValidators.service.ts new file mode 100644 index 000000000..092b6aef8 --- /dev/null +++ b/packages/server-nest/src/modules/Organization/commands/CommandOrganizationValidators.service.ts @@ -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(); + } + } + } +} diff --git a/packages/server-nest/src/modules/Organization/commands/UpdateOrganization.service.ts b/packages/server-nest/src/modules/Organization/commands/UpdateOrganization.service.ts index 1f762ad82..29905e2d5 100644 --- a/packages/server-nest/src/modules/Organization/commands/UpdateOrganization.service.ts +++ b/packages/server-nest/src/modules/Organization/commands/UpdateOrganization.service.ts @@ -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 { + public async execute(organizationDTO: UpdateOrganizationDto): Promise { 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, }, ); } } -} \ No newline at end of file +} diff --git a/packages/server-nest/src/modules/Organization/dtos/Organization.dto.ts b/packages/server-nest/src/modules/Organization/dtos/Organization.dto.ts index 81f5b920b..18f46b2b3 100644 --- a/packages/server-nest/src/modules/Organization/dtos/Organization.dto.ts +++ b/packages/server-nest/src/modules/Organization/dtos/Organization.dto.ts @@ -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; } diff --git a/packages/server-nest/src/modules/PaymentLinks/GetInvoicePaymentLinkMetadata.ts b/packages/server-nest/src/modules/PaymentLinks/GetInvoicePaymentLinkMetadata.ts index 89abc725e..f9660cf95 100644 --- a/packages/server-nest/src/modules/PaymentLinks/GetInvoicePaymentLinkMetadata.ts +++ b/packages/server-nest/src/modules/PaymentLinks/GetInvoicePaymentLinkMetadata.ts @@ -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() diff --git a/packages/server-nest/src/modules/PaymentLinks/GetPaymentLinkInvoicePdf.ts b/packages/server-nest/src/modules/PaymentLinks/GetPaymentLinkInvoicePdf.ts index 7d0d50995..ac70bb3bb 100644 --- a/packages/server-nest/src/modules/PaymentLinks/GetPaymentLinkInvoicePdf.ts +++ b/packages/server-nest/src/modules/PaymentLinks/GetPaymentLinkInvoicePdf.ts @@ -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); } diff --git a/packages/server-nest/src/modules/PaymentLinks/PaymentLinks.module.ts b/packages/server-nest/src/modules/PaymentLinks/PaymentLinks.module.ts index 88a445c5a..122c63970 100644 --- a/packages/server-nest/src/modules/PaymentLinks/PaymentLinks.module.ts +++ b/packages/server-nest/src/modules/PaymentLinks/PaymentLinks.module.ts @@ -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], diff --git a/packages/server-nest/src/modules/Roles/Roles.utils.ts b/packages/server-nest/src/modules/Roles/Roles.utils.ts new file mode 100644 index 000000000..cc73efeb0 --- /dev/null +++ b/packages/server-nest/src/modules/Roles/Roles.utils.ts @@ -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); + } +}; diff --git a/packages/server-nest/src/modules/Roles/commands/CreateRole.service.ts b/packages/server-nest/src/modules/Roles/commands/CreateRole.service.ts index 19e7aaf5f..7ab9b5905 100644 --- a/packages/server-nest/src/modules/Roles/commands/CreateRole.service.ts +++ b/packages/server-nest/src/modules/Roles/commands/CreateRole.service.ts @@ -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; diff --git a/packages/server-nest/src/modules/Roles/commands/DeleteRole.service.ts b/packages/server-nest/src/modules/Roles/commands/DeleteRole.service.ts index ad0c322ce..00d51504b 100644 --- a/packages/server-nest/src/modules/Roles/commands/DeleteRole.service.ts +++ b/packages/server-nest/src/modules/Roles/commands/DeleteRole.service.ts @@ -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, + @Inject(RolePermission.name) private readonly rolePermissionModel: TenantModelProxy< typeof RolePermission >, + + @Inject(TenantUser.name) + private readonly tenantUserModel: TenantModelProxy, ) {} - + /** * Deletes the given role from the storage. * @param {number} roleId - Role id. */ public async deleteRole(roleId: number): Promise { - // 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); + } + }; } diff --git a/packages/server-nest/src/modules/Roles/commands/EditRole.service.ts b/packages/server-nest/src/modules/Roles/commands/EditRole.service.ts index 769debc84..a37a64fdf 100644 --- a/packages/server-nest/src/modules/Roles/commands/EditRole.service.ts +++ b/packages/server-nest/src/modules/Roles/commands/EditRole.service.ts @@ -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, @@ -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. diff --git a/packages/server-nest/src/modules/Roles/dtos/Role.dto.ts b/packages/server-nest/src/modules/Roles/dtos/Role.dto.ts index 2ae8d2af6..9d8f73238 100644 --- a/packages/server-nest/src/modules/Roles/dtos/Role.dto.ts +++ b/packages/server-nest/src/modules/Roles/dtos/Role.dto.ts @@ -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; } diff --git a/packages/server-nest/src/modules/Roles/models/Role.model.ts b/packages/server-nest/src/modules/Roles/models/Role.model.ts index 4a3e8b397..103a790b1 100644 --- a/packages/server-nest/src/modules/Roles/models/Role.model.ts +++ b/packages/server-nest/src/modules/Roles/models/Role.model.ts @@ -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; /** diff --git a/packages/server-nest/src/modules/Roles/queries/GetRole.service.ts b/packages/server-nest/src/modules/Roles/queries/GetRole.service.ts index 85a2752e3..ceb65b8a0 100644 --- a/packages/server-nest/src/modules/Roles/queries/GetRole.service.ts +++ b/packages/server-nest/src/modules/Roles/queries/GetRole.service.ts @@ -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, + ) {} /** * Retrieve the given role metadata. @@ -17,11 +24,11 @@ export class GetRoleService { * @returns {Promise} */ public async getRole(roleId: number): Promise { - 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()); } diff --git a/packages/server-nest/src/modules/Roles/utils.ts b/packages/server-nest/src/modules/Roles/utils.ts index 5e539f3dd..f497d6972 100644 --- a/packages/server-nest/src/modules/Roles/utils.ts +++ b/packages/server-nest/src/modules/Roles/utils.ts @@ -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, + }); + } +}; diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts index d0540bde9..a4ac13327 100644 --- a/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts @@ -119,6 +119,6 @@ import { SaleInvoicesCost } from './SalesInvoicesCost'; InvoicePaymentsGLEntriesRewrite, SaleInvoicesCost, ], - exports: [GetSaleInvoice, SaleInvoicesCost], + exports: [GetSaleInvoice, SaleInvoicesCost, SaleInvoicePdf], }) export class SaleInvoicesModule {} diff --git a/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoicePaymentIntegrationSubscriber.ts b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoicePaymentIntegrationSubscriber.ts index d44b3ad3a..d7d4968ec 100644 --- a/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoicePaymentIntegrationSubscriber.ts +++ b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoicePaymentIntegrationSubscriber.ts @@ -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, diff --git a/packages/server-nest/src/modules/Subscription/Subscription.module.ts b/packages/server-nest/src/modules/Subscription/Subscription.module.ts index ee46b1f4b..973f9d225 100644 --- a/packages/server-nest/src/modules/Subscription/Subscription.module.ts +++ b/packages/server-nest/src/modules/Subscription/Subscription.module.ts @@ -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 {} diff --git a/packages/server-nest/src/modules/Subscription/SubscriptionsLemonWebhook.controller.ts b/packages/server-nest/src/modules/Subscription/SubscriptionsLemonWebhook.controller.ts index 68c13c53c..38b0c718b 100644 --- a/packages/server-nest/src/modules/Subscription/SubscriptionsLemonWebhook.controller.ts +++ b/packages/server-nest/src/modules/Subscription/SubscriptionsLemonWebhook.controller.ts @@ -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); diff --git a/packages/server-nest/src/modules/Subscription/commands/NewSubscription.service.ts b/packages/server-nest/src/modules/Subscription/commands/NewSubscription.service.ts index 1dff0e9c4..f5f84f279 100644 --- a/packages/server-nest/src/modules/Subscription/commands/NewSubscription.service.ts +++ b/packages/server-nest/src/modules/Subscription/commands/NewSubscription.service.ts @@ -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, diff --git a/packages/server-nest/src/modules/Subscription/exceptions/NotAllowedChangeSubscriptionPlan.ts b/packages/server-nest/src/modules/Subscription/exceptions/NotAllowedChangeSubscriptionPlan.ts new file mode 100644 index 000000000..98675ec9a --- /dev/null +++ b/packages/server-nest/src/modules/Subscription/exceptions/NotAllowedChangeSubscriptionPlan.ts @@ -0,0 +1,6 @@ +export class NotAllowedChangeSubscriptionPlan extends Error { + constructor(message: string = 'Not allowed to change subscription plan.') { + super(message); + this.name = 'NotAllowedChangeSubscriptionPlan'; + } +} diff --git a/packages/server-nest/src/modules/Subscription/models/Plan.ts b/packages/server-nest/src/modules/Subscription/models/Plan.ts index 40f953cbf..d4ed16a02 100644 --- a/packages/server-nest/src/modules/Subscription/models/Plan.ts +++ b/packages/server-nest/src/modules/Subscription/models/Plan.ts @@ -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; diff --git a/packages/server-nest/src/modules/Subscription/models/PlanSubscription.ts b/packages/server-nest/src/modules/Subscription/models/PlanSubscription.ts index 4f96e297f..ff561ab78 100644 --- a/packages/server-nest/src/modules/Subscription/models/PlanSubscription.ts +++ b/packages/server-nest/src/modules/Subscription/models/PlanSubscription.ts @@ -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. diff --git a/packages/server-nest/src/modules/Subscription/repositories/PlanSubscription.repository.ts b/packages/server-nest/src/modules/Subscription/repositories/PlanSubscription.repository.ts new file mode 100644 index 000000000..9a4596e5b --- /dev/null +++ b/packages/server-nest/src/modules/Subscription/repositories/PlanSubscription.repository.ts @@ -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 = { + tenantId, + slug: subscriptionSlug, + planId, + startsAt: period.getStartDate(), + endsAt: period.getEndDate(), + lemonSubscriptionId: payload?.lemonSqueezyId || null, + }; + return this.model.query().insert({ ...model }); + } +} diff --git a/packages/server-nest/src/modules/Subscription/types.ts b/packages/server-nest/src/modules/Subscription/types.ts index 09d585945..fe0b3ba82 100644 --- a/packages/server-nest/src/modules/Subscription/types.ts +++ b/packages/server-nest/src/modules/Subscription/types.ts @@ -8,7 +8,7 @@ export const ERRORS = { }; export interface IOrganizationSubscriptionChanged { - lemonSubscriptionId: number; + lemonSubscriptionId: string; newVariantId: number; } diff --git a/packages/server-nest/src/modules/System/models/TenantModel.ts b/packages/server-nest/src/modules/System/models/TenantModel.ts index b508b5a70..dff3a2047 100644 --- a/packages/server-nest/src/modules/System/models/TenantModel.ts +++ b/packages/server-nest/src/modules/System/models/TenantModel.ts @@ -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; /** * 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', + }, + }, }; } } diff --git a/packages/server-nest/src/modules/System/repositories/Tenant.repository.ts b/packages/server-nest/src/modules/System/repositories/Tenant.repository.ts new file mode 100644 index 000000000..4107035a0 --- /dev/null +++ b/packages/server-nest/src/modules/System/repositories/Tenant.repository.ts @@ -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} metadata - The metadata to save. + */ + async saveMetadata(tenantId: number, metadata: Record) { + 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'), + }); + } +} diff --git a/packages/server-nest/src/modules/TenantDBManager/TenantDBManager.module.ts b/packages/server-nest/src/modules/TenantDBManager/TenantDBManager.module.ts index 64b47e312..0c3086d2e 100644 --- a/packages/server-nest/src/modules/TenantDBManager/TenantDBManager.module.ts +++ b/packages/server-nest/src/modules/TenantDBManager/TenantDBManager.module.ts @@ -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]; +} diff --git a/packages/server-nest/src/modules/TenantDBManager/TenantDBManager.ts b/packages/server-nest/src/modules/TenantDBManager/TenantDBManager.ts index 410d7baf4..3ae899e00 100644 --- a/packages/server-nest/src/modules/TenantDBManager/TenantDBManager.ts +++ b/packages/server-nest/src/modules/TenantDBManager/TenantDBManager.ts @@ -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} */ - 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} */ - public async createDatabase(tenant: ITenant): Promise { + public async createDatabase(tenant: TenantModel): Promise { 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} */ - public async migrate(tenant: ITenant): Promise { + public async migrate(tenant: TenantModel): Promise { 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} */ - public async seed(tenant: ITenant): Promise { + public async seed(tenant: TenantModel): Promise { 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} */ - async throwErrorIfTenantDBExists(tenant: ITenant) { + async throwErrorIfTenantDBExists(tenant: TenantModel) { const isExists = await this.databaseExists(tenant); if (isExists) { throw new TenantDBAlreadyExists(); diff --git a/packages/server-nest/src/modules/TenantDBManager/TenantsManager.ts b/packages/server-nest/src/modules/TenantDBManager/TenantsManager.ts index 758f5a267..e5a673ad0 100644 --- a/packages/server-nest/src/modules/TenantDBManager/TenantsManager.ts +++ b/packages/server-nest/src/modules/TenantDBManager/TenantsManager.ts @@ -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} + * @return {Promise} */ - public async createTenant(): Promise { - const { tenantRepository } = this.sysRepositories; - const tenant = await tenantRepository.createWithUniqueOrgId(); - - return tenant; + public async createTenant(): Promise { + return this.tenantRepository.createWithUniqueOrgId(); } /** @@ -51,8 +32,8 @@ export class TenantsManagerService { * @param {ITenant} tenant - * @return {Promise} */ - public async createDatabase(tenant: ITenant): Promise { - this.throwErrorIfTenantAlreadyInitialized(tenant); + public async createDatabase(tenant: TenantModel): Promise { + 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} */ - public async hasDatabase(tenant: ITenant): Promise { + public async hasDatabase(tenant: TenantModel): Promise { return this.tenantDbManager.databaseExists(tenant); } @@ -82,18 +63,18 @@ export class TenantsManagerService { * @param {ITenant} tenant * @return {Promise} */ - public async migrateTenant(tenant: ITenant): Promise { + public async migrateTenant(tenant: TenantModel): Promise { // 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} */ - public async seedTenant(tenant: ITenant, tenancyContext): Promise { + public async seedTenant(tenant: TenantModel, tenancyContext): Promise { // 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); } } diff --git a/packages/server-nest/src/modules/TenantDBManager/_utils.ts b/packages/server-nest/src/modules/TenantDBManager/_utils.ts new file mode 100644 index 000000000..2181e0ee2 --- /dev/null +++ b/packages/server-nest/src/modules/TenantDBManager/_utils.ts @@ -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(); + } +}; diff --git a/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantAlreadyInitialized.ts b/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantAlreadyInitialized.ts new file mode 100644 index 000000000..55bfc89b3 --- /dev/null +++ b/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantAlreadyInitialized.ts @@ -0,0 +1,6 @@ +export class TenantAlreadyInitialized extends Error { + constructor(description: string = 'Tenant is already initialized') { + super(description); + this.name = 'TenantAlreadyInitialized'; + } +} diff --git a/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantAlreadySeeded.ts b/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantAlreadySeeded.ts new file mode 100644 index 000000000..170c01ced --- /dev/null +++ b/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantAlreadySeeded.ts @@ -0,0 +1,6 @@ +export class TenantAlreadySeeded extends Error { + constructor(description: string = 'Tenant is already seeded') { + super(description); + this.name = 'TenantAlreadySeeded'; + } +} diff --git a/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantDBAlreadyExists.ts b/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantDBAlreadyExists.ts index e21da82ce..13bc47672 100644 --- a/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantDBAlreadyExists.ts +++ b/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantDBAlreadyExists.ts @@ -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'; } -} \ No newline at end of file +} diff --git a/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantDatabaseNotBuilt.ts b/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantDatabaseNotBuilt.ts new file mode 100644 index 000000000..c736721d0 --- /dev/null +++ b/packages/server-nest/src/modules/TenantDBManager/exceptions/TenantDatabaseNotBuilt.ts @@ -0,0 +1,6 @@ +export class TenantDatabaseNotBuilt extends Error { + constructor(description: string = 'Tenant database is not built yet.') { + super(description); + this.name = 'TenantDatabaseNotBuilt'; + } +} diff --git a/packages/server-nest/tsconfig.build.json b/packages/server-nest/tsconfig.build.json index c18e810dd..6bde35e9e 100644 --- a/packages/server-nest/tsconfig.build.json +++ b/packages/server-nest/tsconfig.build.json @@ -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" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c69d22611..4b12c820c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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