diff --git a/packages/server/bin/bigcapital.js b/packages/server/bin/bigcapital.js new file mode 100644 index 000000000..5224d0dc5 --- /dev/null +++ b/packages/server/bin/bigcapital.js @@ -0,0 +1,282 @@ +#!/usr/bin/env node +const commander = require('commander'); +const color = require('colorette'); +const argv = require('getopts'); +const Knex = require('knex'); +const { knexSnakeCaseMappers } = require('objection'); +const config = require('../src/config'); + +function initSystemKnex() { + return Knex({ + client: config.system.db_client, + connection: { + host: config.system.db_host, + user: config.system.db_user, + password: config.system.db_password, + database: config.system.db_name, + charset: 'utf8', + }, + migrations: { + directory: config.system.migrations_dir, + }, + seeds: { + directory: config.system.seeds_dir, + }, + pool: { min: 0, max: 7 }, + ...knexSnakeCaseMappers({ upperCase: true }), + }); +} + +function initTenantKnex(organizationId) { + return Knex({ + client: config.tenant.db_client, + connection: { + host: config.tenant.db_host, + user: config.tenant.db_user, + password: config.tenant.db_password, + database: `${config.tenant.db_name_prefix}${organizationId}`, + charset: config.tenant.charset, + }, + migrations: { + directory: config.tenant.migrations_dir, + }, + seeds: { + directory: config.tenant.seeds_dir, + }, + pool: { min: 0, max: 5 }, + ...knexSnakeCaseMappers({ upperCase: true }), + }); +} +function exit(text) { + if (text instanceof Error) { + console.error( + color.red(`${text.detail ? `${text.detail}\n` : ''}${text.stack}`) + ); + } else { + console.error(color.red(text)); + } + process.exit(1); +} + +function success(text) { + console.log(text); + process.exit(0); +} +function log(text) { + console.log(text); +} + +function getAllSystemTenants(knex) { + return knex('tenants'); +} + +// module.exports = { +// log, +// success, +// exit, +// initSystemKnex, +// }; + +// - bigcapital system:migrate:latest +// - bigcapital system:migrate:rollback +// - bigcapital tenants:migrate:latest +// - bigcapital tenants:migrate:latest --tenant_id=XXX +// - bigcapital tenants:migrate:rollback +// - bigcapital tenants:migrate:rollback --tenant_id=XXX +// - bigcapital tenants:migrate:make +// - bigcapital system:migrate:make +// - bigcapital tenants:list + +commander + .command('system:migrate:rollback') + .description('Migrate the system database of the application.') + .action(async () => { + try { + const sysKnex = await initSystemKnex(); + const [batchNo, _log] = await sysKnex.migrate.rollback(); + + if (_log.length === 0) { + success(color.cyan('Already at the base migration')); + } + success( + color.green(`Batch ${batchNo} rolled back: ${_log.length} migrations`) + + (argv.verbose ? `\n${color.cyan(_log.join('\n'))}` : '') + ); + } catch (error) { + exit(error); + } + }); + +commander + .command('system:migrate:latest') + .description('Migrate latest mgiration of the system database.') + .action(async () => { + try { + const sysKnex = await initSystemKnex(); + const [batchNo, log] = await sysKnex.migrate.latest(); + + if (log.length === 0) { + success(color.cyan('Already up to date')); + } + success( + color.green(`Batch ${batchNo} run: ${log.length} migrations`) + + (argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '') + ); + } catch (error) { + exit(error); + } + }); + +commander + .command('system:migrate:make ') + .description('Created a named migration file to the system database.') + .action(async (name) => { + const sysKnex = await initSystemKnex(); + + sysKnex.migrate + .make(name) + .then((name) => { + success(color.green(`Created Migration: ${name}`)); + }) + .catch(exit); + }); + +commander + .command('tenants:list') + .description('Retrieve a list of all system tenants databases.') + .action(async (cmd) => { + try { + const sysKnex = await initSystemKnex(); + const tenants = await getAllSystemTenants(sysKnex); + + tenants.forEach((tenant) => { + const dbName = `${config.tenant.db_name_prefix}${tenant.organizationId}`; + console.log( + `ID: ${tenant.id} | Organization ID: ${tenant.organizationId} | DB Name: ${dbName}` + ); + }); + } catch (error) { + exit(error); + } + success('---'); + }); + +commander + .command('tenants:migrate:make ') + .description('Created a named migration file to the tenants database.') + .action(async (name) => { + const sysKnex = await initTenantKnex(); + + sysKnex.migrate + .make(name) + .then((name) => { + success(color.green(`Created Migration: ${name}`)); + }) + .catch(exit); + }); + +commander + .command('tenants:migrate:latest') + .description('Migrate all tenants or the given tenant id.') + .option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.') + .action(async (cmd) => { + try { + const sysKnex = await initSystemKnex(); + const tenants = await getAllSystemTenants(sysKnex); + const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId); + + if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) { + exit(`The given tenant id ${cmd.tenant_id} is not exists.`); + } + // Validate the tenant id exist first of all. + const migrateOpers = []; + const migrateTenant = async (organizationId) => { + try { + const tenantKnex = await initTenantKnex(organizationId); + const [batchNo, _log] = await tenantKnex.migrate.latest(); + + const tenantDb = `${config.tenant.db_name_prefix}${organizationId}`; + + if (_log.length === 0) { + log(color.cyan('Already up to date')); + } + log( + color.green( + `Tenant ${tenantDb} > Batch ${batchNo} run: ${_log.length} migrations` + ) + (argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '') + ); + log('-------------------'); + } catch (error) { + log(error); + } + }; + if (!cmd.tenant_id) { + tenants.forEach((tenant) => { + const oper = migrateTenant(tenant.organizationId); + migrateOpers.push(oper); + }); + } else { + const oper = migrateTenant(cmd.tenant_id); + migrateOpers.push(oper); + } + + Promise.all(migrateOpers).then(() => { + success('All tenants are migrated.'); + }); + } catch (error) { + exit(error); + } + }); + +commander + .command('tenants:migrate:rollback') + .description('Rollback the last batch of tenants migrations.') + .option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.') + .action(async (cmd) => { + try { + const sysKnex = await initSystemKnex(); + const tenants = await getAllSystemTenants(sysKnex); + const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId); + + if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) { + exit(`The given tenant id ${cmd.tenant_id} is not exists.`); + } + + const migrateOpers = []; + const migrateTenant = async (organizationId) => { + try { + const tenantKnex = await initTenantKnex(organizationId); + const [batchNo, _log] = await tenantKnex.migrate.rollback(); + const tenantDb = `${config.tenant.db_name_prefix}${organizationId}`; + + if (_log.length === 0) { + log(color.cyan('Already at the base migration')); + } + log( + color.green( + `Tenant: ${tenantDb} > Batch ${batchNo} rolled back: ${_log.length} migrations` + ) + (argv.verbose ? `\n${color.cyan(_log.join('\n'))}` : '') + ); + log('---------------'); + } catch (error) { + exit(error); + } + }; + + if (!cmd.tenant_id) { + tenants.forEach((tenant) => { + const oper = migrateTenant(tenant.organizationId); + migrateOpers.push(oper); + }); + } else { + const oper = migrateTenant(cmd.tenant_id); + migrateOpers.push(oper); + } + Promise.all(migrateOpers).then(() => { + success('All tenants are rollbacked.'); + }); + } catch (error) { + exit(error); + } + }); +commander.parse(); diff --git a/packages/server/bin/utils.js b/packages/server/bin/utils.js new file mode 100644 index 000000000..82c4d8cde --- /dev/null +++ b/packages/server/bin/utils.js @@ -0,0 +1,51 @@ +const Knex = require('knex'); +const { knexSnakeCaseMappers } = require('objection'); +const color = require('colorette'); +const config = require('./config'); + +function initSystemKnex() { + return Knex({ + client: config.system.db_client, + connection: { + host: config.system.db_host, + user: config.system.db_user, + password: config.system.db_password, + database: config.system.db_name, + charset: 'utf8', + }, + migrations: { + directory: config.system.migrations_dir, + }, + seeds: { + directory: config.system.seeds_dir, + }, + pool: { min: 0, max: 7 }, + ...knexSnakeCaseMappers({ upperCase: true }), + }); +} + +function exit(text) { + if (text instanceof Error) { + console.error( + color.red(`${text.detail ? `${text.detail}\n` : ''}${text.stack}`) + ); + } else { + console.error(color.red(text)); + } + process.exit(1); +} + +function success(text) { + console.log(text); + process.exit(0); +} +function log(text) { + console.log(text); +} + +module.exports = { + log, + success, + exit, + initSystemKnex, +}; diff --git a/packages/server/src/config/index.ts b/packages/server/src/config/index.js similarity index 98% rename from packages/server/src/config/index.ts rename to packages/server/src/config/index.js index b9ff6286c..6bc4e0116 100644 --- a/packages/server/src/config/index.ts +++ b/packages/server/src/config/index.js @@ -1,4 +1,4 @@ -import dotenv from 'dotenv'; +const dotenv = require('dotenv'); // Set the NODE_ENV to 'development' by default // process.env.NODE_ENV = process.env.NODE_ENV || 'development'; @@ -9,7 +9,7 @@ if (envFound.error) { throw new Error("⚠️ Couldn't find .env file ⚠️"); } -export default { +module.exports = { /** * Your favorite port */ diff --git a/packages/webapp/package-lock.json b/packages/webapp/package-lock.json index fa96c39ab..1852ca0f5 100644 --- a/packages/webapp/package-lock.json +++ b/packages/webapp/package-lock.json @@ -1144,6 +1144,11 @@ "@babel/plugin-transform-typescript": "^7.9.0" } }, + "@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + }, "@babel/runtime": { "version": "7.20.13", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", @@ -3516,9 +3521,9 @@ } }, "async-each": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.5.tgz", - "integrity": "sha512-5QzqtU3BlagehwmdoqwaS2FBQF2P5eL6vFqXwNsb5jwoEsmtfAXg1ocFvW7I6/gGLFhBMKwcMwZuy7uv/Bo9jA==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==" }, "async-foreach": { "version": "0.1.3", @@ -4681,9 +4686,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001450", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz", - "integrity": "sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew==" + "version": "1.0.30001451", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001451.tgz", + "integrity": "sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w==" }, "capture-exit": { "version": "2.0.0", @@ -6015,9 +6020,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.285", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.285.tgz", - "integrity": "sha512-47o4PPgxfU1KMNejz+Dgaodf7YTcg48uOfV1oM6cs3adrl2+7R+dHkt3Jpxqo0LRCbGJEzTKMUt0RdvByb/leg==" + "version": "1.4.289", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.289.tgz", + "integrity": "sha512-relLdMfPBxqGCxy7Gyfm1HcbRPcFUJdlgnCPVgQ23sr1TvUrRJz0/QPoGP0+x41wOVSTN/Wi3w6YDgHiHJGOzg==" }, "elliptic": { "version": "6.5.4", @@ -10984,9 +10989,9 @@ } }, "node-releases": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.9.tgz", - "integrity": "sha512-2xfmOrRkGogbTK9R6Leda0DGiXeY3p2NJpy4+gNCffdUvV6mdEJnaDEic1i3Ec2djAo8jWYoJMR5PB0MSMpxUA==" + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==" }, "node-sass": { "version": "4.14.1", @@ -14129,23 +14134,18 @@ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" }, "regexpu-core": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", - "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.0.tgz", + "integrity": "sha512-ZdhUQlng0RoscyW7jADnUZ25F5eVtHdMyXSb2PiwafvteRAOJUjFoUPEYZSIfP99fBIs3maLIRfpEddT78wAAQ==", "requires": { + "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.1.0", - "regjsgen": "^0.7.1", "regjsparser": "^0.9.1", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" } }, - "regjsgen": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", - "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==" - }, "regjsparser": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz",