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