mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
feat: remove path alias.
feat: remove Webpack and depend on nodemon. feat: refactoring expenses. feat: optimize system users with caching. feat: architecture tenant optimize.
This commit is contained in:
@@ -1,138 +0,0 @@
|
||||
import Knex from 'knex';
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
import { Service } from 'typedi';
|
||||
import Tenant from '@/system/models/Tenant';
|
||||
import config from '@/../config/config';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import uniqid from 'uniqid';
|
||||
import dbManager from '@/database/manager';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import SystemUser from '@/system/models/SystemUser';
|
||||
import TenantUser from '@/models/TenantUser';
|
||||
// import TenantModel from '@/models/TenantModel';
|
||||
|
||||
// const TenantWebsite: {
|
||||
// tenantDb: Knex,
|
||||
// tenantId: Number,
|
||||
// tenantOrganizationId: String,
|
||||
// }
|
||||
|
||||
@Service()
|
||||
export default class TenantsManager {
|
||||
|
||||
constructor() {
|
||||
this.knexCache = new Map();
|
||||
}
|
||||
|
||||
async getTenant(organizationId) {
|
||||
const tenant = await Tenant.query()
|
||||
.where('organization_id', organizationId).first();
|
||||
|
||||
return tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tenant database.
|
||||
* @param {Integer} uniqId
|
||||
* @return {TenantWebsite}
|
||||
*/
|
||||
async createTenant(uniqId) {
|
||||
const organizationId = uniqId || uniqid();
|
||||
const tenantOrganization = await Tenant.query().insert({
|
||||
organization_id: organizationId,
|
||||
});
|
||||
|
||||
const tenantDbName = `bigcapital_tenant_${organizationId}`;
|
||||
await dbManager.createDb(tenantDbName);
|
||||
|
||||
const tenantDb = TenantsManager.knexInstance(organizationId);
|
||||
await tenantDb.migrate.latest();
|
||||
|
||||
return {
|
||||
tenantDb,
|
||||
tenantId: tenantOrganization.id,
|
||||
organizationId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop tenant database of the given tenant website.
|
||||
* @param {TenantWebsite} tenantWebsite
|
||||
*/
|
||||
async dropTenant(tenantWebsite) {
|
||||
const tenantDbName = `bigcapital_tenant_${tenantWebsite.organizationId}`;
|
||||
await dbManager.dropDb(tenantDbName);
|
||||
|
||||
await SystemUser.query()
|
||||
.where('tenant_id', tenantWebsite.tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a user that associate to the given tenant.
|
||||
*/
|
||||
async createTenantUser(tenantWebsite, user) {
|
||||
const userInsert = { ...user };
|
||||
|
||||
const systemUser = await SystemUser.query().insert({
|
||||
...user,
|
||||
tenant_id: tenantWebsite.tenantId,
|
||||
});
|
||||
TenantModel.knexBinded = tenantWebsite.tenantDb;
|
||||
|
||||
const tenantUser = await TenantUser.bindKnex(tenantWebsite.tenantDb)
|
||||
.query()
|
||||
.insert({
|
||||
...omit(userInsert, ['password']),
|
||||
});
|
||||
return {
|
||||
...tenantUser,
|
||||
...systemUser
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all tenants metadata from system storage.
|
||||
*/
|
||||
getAllTenants() {
|
||||
return Tenant.query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given organization id knex configuration.
|
||||
* @param {String} organizationId -
|
||||
*/
|
||||
getTenantKnexConfig(organizationId) {
|
||||
return {
|
||||
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 },
|
||||
};
|
||||
}
|
||||
|
||||
knexInstance(organizationId) {
|
||||
const knexCache = new Map();
|
||||
let knex = knexCache.get(organizationId);
|
||||
|
||||
if (!knex) {
|
||||
knex = Knex({
|
||||
...this.getTenantKnexConfig(organizationId),
|
||||
...knexSnakeCaseMappers({ upperCase: true }),
|
||||
});
|
||||
knexCache.set(organizationId, knex);
|
||||
}
|
||||
return knex;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@ exports.up = function(knex) {
|
||||
return knex.schema.createTable('tenants', (table) => {
|
||||
table.bigIncrements();
|
||||
table.string('organization_id');
|
||||
table.boolean('initialized').defaultTo(false);
|
||||
|
||||
table.dateTime('under_maintenance_since').nullable();
|
||||
table.dateTime('initialized_at').nullable();
|
||||
table.dateTime('seeded_at').nullable();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import SystemModel from 'system/models/SystemModel';
|
||||
|
||||
export default class UserInvite extends SystemModel {
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import SystemModel from 'system/models/SystemModel';
|
||||
|
||||
export default class PasswordResets extends SystemModel {
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import moment from 'moment';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import { ILicensesFilter } from '@/interfaces';
|
||||
import SystemModel from 'system/models/SystemModel';
|
||||
import { ILicensesFilter } from 'interfaces';
|
||||
|
||||
export default class License extends SystemModel {
|
||||
/**
|
||||
@@ -61,7 +61,7 @@ export default class License extends SystemModel {
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Plan = require('@/system/models/Subscriptions/Plan');
|
||||
const Plan = require('system/models/Subscriptions/Plan');
|
||||
|
||||
return {
|
||||
plan: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import SystemModel from 'system/models/SystemModel';
|
||||
import { PlanSubscription } from '..';
|
||||
|
||||
export default class Plan extends mixin(SystemModel) {
|
||||
@@ -39,7 +39,7 @@ export default class Plan extends mixin(SystemModel) {
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const PlanFeature = require('@/system/models/Subscriptions/PlanFeature');
|
||||
const PlanFeature = require('system/models/Subscriptions/PlanFeature');
|
||||
|
||||
return {
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import SystemModel from 'system/models/SystemModel';
|
||||
|
||||
export default class PlanFeature extends mixin(SystemModel) {
|
||||
/**
|
||||
@@ -20,7 +20,7 @@ export default class PlanFeature extends mixin(SystemModel) {
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Plan = require('@/system/models/Subscriptions/Plan');
|
||||
const Plan = require('system/models/Subscriptions/Plan');
|
||||
|
||||
return {
|
||||
plan: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import SystemModel from 'system/models/SystemModel';
|
||||
import moment from 'moment';
|
||||
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||
import SubscriptionPeriod from 'services/Subscription/SubscriptionPeriod';
|
||||
|
||||
export default class PlanSubscription extends mixin(SystemModel) {
|
||||
/**
|
||||
@@ -67,8 +67,8 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
||||
* Relations mappings.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Tenant = require('@/system/Models/Tenant');
|
||||
const Plan = require('@/system/Models/Subscriptions/Plan');
|
||||
const Tenant = require('system/Models/Tenant');
|
||||
const Plan = require('system/Models/Subscriptions/Plan');
|
||||
|
||||
return {
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BaseModel from '@/models/Model';
|
||||
import BaseModel from 'models/Model';
|
||||
|
||||
export default class SystemModel extends BaseModel{
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import SoftDelete from 'objection-soft-delete';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import SystemModel from 'system/models/SystemModel';
|
||||
import moment from 'moment';
|
||||
|
||||
export default class SystemUser extends mixin(SystemModel, [SoftDelete({
|
||||
@@ -27,7 +27,7 @@ export default class SystemUser extends mixin(SystemModel, [SoftDelete({
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Tenant = require('@/system/models/Tenant');
|
||||
const Tenant = require('system/models/Tenant');
|
||||
|
||||
return {
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import BaseModel from '@/models/Model';
|
||||
import BaseModel from 'models/Model';
|
||||
import { Model } from 'objection';
|
||||
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||
import SubscriptionPeriod from 'services/Subscription/SubscriptionPeriod';
|
||||
|
||||
export default class Tenant extends BaseModel {
|
||||
/**
|
||||
|
||||
21
server/src/system/repositories/SubscriptionRepository.ts
Normal file
21
server/src/system/repositories/SubscriptionRepository.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import SystemRepository from "system/repositories/SystemRepository";
|
||||
import { PlanSubscription } from 'system/models'
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionRepository extends SystemRepository{
|
||||
@Inject('cache')
|
||||
cache: any;
|
||||
|
||||
/**
|
||||
* Retrieve subscription from a given slug in specific tenant.
|
||||
* @param {string} slug
|
||||
* @param {number] tenantId
|
||||
*/
|
||||
getBySlugInTenant(slug: string, tenantId: number) {
|
||||
const key = `subscription.slug.${slug}.tenant.${tenantId}`;
|
||||
return this.cache.get(key, () => {
|
||||
return PlanSubscription.query().findOne('slug', slug).where('tenant_id', tenantId);
|
||||
});
|
||||
}
|
||||
}
|
||||
5
server/src/system/repositories/SystemRepository.ts
Normal file
5
server/src/system/repositories/SystemRepository.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export default class SystemRepository {
|
||||
|
||||
}
|
||||
137
server/src/system/repositories/SystemUserRepository.ts
Normal file
137
server/src/system/repositories/SystemUserRepository.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import SystemRepository from "system/repositories/SystemRepository";
|
||||
import { SystemUser } from "system/models";
|
||||
import { ISystemUser } from 'interfaces';
|
||||
|
||||
@Service()
|
||||
export default class SystemUserRepository extends SystemRepository {
|
||||
@Inject('cache')
|
||||
cache: any;
|
||||
|
||||
/**
|
||||
* Patches the last login date to the given system user.
|
||||
* @param {number} userId
|
||||
*/
|
||||
async patchLastLoginAt(userId: number) {
|
||||
const user = await SystemUser.query().patchAndFetchById(userId, {
|
||||
last_login_at: moment().toMySqlDateTime()
|
||||
});
|
||||
this.flushUserCache(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds system user by crediential.
|
||||
* @param {string} crediential - Phone number or email.
|
||||
* @return {ISystemUser}
|
||||
*/
|
||||
findByCrediential(crediential: string) {
|
||||
return SystemUser.query().whereNotDeleted()
|
||||
.findOne('email', crediential)
|
||||
.orWhere('phone_number', crediential);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve system user details of the given id.
|
||||
* @param {number} userId
|
||||
*/
|
||||
getById(userId: number) {
|
||||
return this.cache.get(`systemUser.id.${userId}`, () => {
|
||||
return SystemUser.query().whereNotDeleted().findById(userId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve user by id and tenant id.
|
||||
* @param {number} userId
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
getByIdAndTenant(userId: number, tenantId: number) {
|
||||
return this.cache.get(`systemUser.id.${userId}.tenant.${tenantId}`, () => {
|
||||
return SystemUser.query().whereNotDeleted()
|
||||
.findOne({ id: userId, tenant_id: tenantId });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve system user details by the given email.
|
||||
* @param {string} email
|
||||
*/
|
||||
getByEmail(email: string) {
|
||||
return this.cache.get(`systemUser.email.${email}`, () => {
|
||||
return SystemUser.query().whereNotDeleted().findOne('email', email);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve user by phone number.
|
||||
* @param {string} phoneNumber
|
||||
*/
|
||||
getByPhoneNumber(phoneNumber: string) {
|
||||
return this.cache.get(`systemUser.phoneNumber.${phoneNumber}`, () => {
|
||||
return SystemUser.query().whereNotDeleted().findOne('phoneNumber', phoneNumber);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits details.
|
||||
* @param {number} userId
|
||||
* @param {number} user
|
||||
*/
|
||||
edit(userId: number, userInput: ISystemUser) {
|
||||
const user = SystemUser.query().patchAndFetchById(userId, { ...userInput });
|
||||
this.flushUserCache(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user.
|
||||
* @param {IUser} userInput
|
||||
*/
|
||||
create(userInput: ISystemUser) {
|
||||
return SystemUser.query().insert({ ...userInput });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes user by the given id.
|
||||
* @param {number} userId
|
||||
*/
|
||||
async deleteById(userId: number) {
|
||||
const user = this.getById(userId);
|
||||
await SystemUser.query().where('id', userId).delete();
|
||||
this.flushUserCache(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate user by the given id.
|
||||
* @param {number} userId
|
||||
*/
|
||||
async activateById(userId: number) {
|
||||
const user = await SystemUser.query().patchAndFetchById(userId, { active: 1 });
|
||||
this.flushUserCache(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivate user by the given id.
|
||||
* @param {number} userId
|
||||
*/
|
||||
async inactivateById(userId: number) {
|
||||
const user = await SystemUser.query().patchAndFetchById(userId, { active: 0 });
|
||||
this.flushUserCache(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush user cache.
|
||||
* @param {IUser} user
|
||||
*/
|
||||
flushUserCache(user: ISystemUser) {
|
||||
this.cache.del(`systemUser.phoneNumber.${user.phoneNumber}`);
|
||||
this.cache.del(`systemUser.email.${user.email}`);
|
||||
|
||||
this.cache.del(`systemUser.id.${user.id}`);
|
||||
this.cache.del(`systemUser.id.${user.id}.tenant.${user.tenantId}`);
|
||||
}
|
||||
}
|
||||
73
server/src/system/repositories/TenantRepository.ts
Normal file
73
server/src/system/repositories/TenantRepository.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Inject } from 'typedi';
|
||||
import moment from "moment";
|
||||
import { Tenant } from 'system/models';
|
||||
import SystemRepository from "./SystemRepository";
|
||||
import { ITenant } from 'interfaces';
|
||||
import uniqid from 'uniqid';
|
||||
|
||||
export default class TenantRepository extends SystemRepository {
|
||||
@Inject('cache')
|
||||
cache: any;
|
||||
|
||||
/**
|
||||
* Flush the given tenant stored cache.
|
||||
* @param {ITenant} tenant
|
||||
*/
|
||||
flushTenantCache(tenant: ITenant) {
|
||||
this.cache.del(`tenant.org.${tenant.organizationId}`);
|
||||
this.cache.del(`tenant.id.${tenant.id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tenant with random organization id.
|
||||
* @return {ITenant}
|
||||
*/
|
||||
newTenantWithUniqueOrgId(uniqId?: string): Promise<ITenant>{
|
||||
const organizationId = uniqid() || uniqId;
|
||||
return Tenant.query().insert({ organizationId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark as seeded.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
async markAsSeeded(tenantId: number) {
|
||||
const tenant = await Tenant.query()
|
||||
.patchAndFetchById(tenantId, {
|
||||
seeded_at: moment().toMySqlDateTime(),
|
||||
});
|
||||
this.flushTenantCache(tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the the given organization as initialized.
|
||||
* @param {string} organizationId
|
||||
*/
|
||||
async markAsInitialized(tenantId: number) {
|
||||
const tenant = await Tenant.query()
|
||||
.patchAndFetchById(tenantId, {
|
||||
initialized_at: moment().toMySqlDateTime(),
|
||||
});
|
||||
this.flushTenantCache(tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve tenant details by the given organization id.
|
||||
* @param {string} organizationId
|
||||
*/
|
||||
getByOrgId(organizationId: string) {
|
||||
return this.cache.get(`tenant.org.${organizationId}`, () => {
|
||||
return Tenant.query().findOne('organization_id', organizationId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve tenant details by the given tenant id.
|
||||
* @param {string} tenantId
|
||||
*/
|
||||
getById(tenantId: number) {
|
||||
return this.cache.get(`tenant.id.${tenantId}`, () => {
|
||||
return Tenant.query().findById(tenantId);
|
||||
});
|
||||
}
|
||||
}
|
||||
9
server/src/system/repositories/index.ts
Normal file
9
server/src/system/repositories/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import SystemUserRepository from 'system/repositories/SystemUserRepository';
|
||||
import SubscriptionRepository from 'system/repositories/SubscriptionRepository';
|
||||
import TenantRepository from 'system/repositories/TenantRepository';
|
||||
|
||||
export {
|
||||
SystemUserRepository,
|
||||
SubscriptionRepository,
|
||||
TenantRepository,
|
||||
};
|
||||
Reference in New Issue
Block a user