feat: Cachable and date session model.

This commit is contained in:
Ahmed Bouhuolia
2020-05-20 06:51:34 +02:00
parent 10f636d2bc
commit 90dc83c70a
18 changed files with 638 additions and 200 deletions

View File

@@ -42,8 +42,8 @@ exports.seed = (knex) => {
name: 'Equity',
normal: 'credit',
root_type: 'equity',
balance_sheet: false,
income_sheet: true,
balance_sheet: true,
income_sheet: false,
},
{
id: 6,

View File

@@ -192,7 +192,7 @@ export default {
async handler(req, res) {
const { id } = req.params;
const { Account } = req.models;
const account = await Account.query().where('id', id).first();
const account = await Account.query().remember().where('id', id).first();
if (!account) {
return res.boom.notFound();
@@ -273,6 +273,7 @@ export default {
const errorReasons = [];
const accountsResource = await Resource.query()
.remember()
.where('name', 'accounts')
.withGraphFetched('fields')
.first();
@@ -294,6 +295,8 @@ export default {
builder.withGraphFetched('roles.field');
builder.withGraphFetched('columns');
builder.first();
builder.remember();
});
const dynamicFilter = new DynamicFilter(Account.tableName);
@@ -335,13 +338,15 @@ export default {
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const accounts = await Account.query().onBuild((builder) => {
builder.modify('filterAccountTypes', filter.account_types);
builder.withGraphFetched('type');
builder.withGraphFetched('balance');
const accounts = await Account.query()
.remember()
.onBuild((builder) => {
builder.modify('filterAccountTypes', filter.account_types);
builder.withGraphFetched('type');
builder.withGraphFetched('balance');
dynamicFilter.buildQuery()(builder);
});
dynamicFilter.buildQuery()(builder);
});
const nestedAccounts = new NestedSet(accounts, { parentId: 'parentAccountId' });
const nestedSetAccounts = nestedAccounts.toTree();

View File

@@ -107,6 +107,7 @@ export default {
filter.account_ids = filter.account_ids.map((id) => parseInt(id, 10));
const accountsJournalEntries = await AccountTransaction.query()
.remember()
.modify('filterDateRange', filter.from_date, filter.to_date)
.modify('filterAccounts', filter.account_ids)
.modify('filterTransactionTypes', filter.transaction_types)
@@ -197,6 +198,7 @@ export default {
return res.status(400).send({ error: errorReasons });
}
const accounts = await Account.query()
.remember('general_ledger_accounts')
.orderBy('index', 'DESC')
.modify('filterAccounts', filter.accounts_ids)
.withGraphFetched('type')
@@ -206,11 +208,13 @@ export default {
});
const openingBalanceTransactions = await AccountTransaction.query()
.remember()
.modify('filterDateRange', null, filter.from_date)
.modify('sumationCreditDebit')
.withGraphFetched('account.type');
const closingBalanceTransactions = await AccountTransaction.query()
.remember()
.modify('filterDateRange', null, filter.to_date)
.modify('sumationCreditDebit')
.withGraphFetched('account.type');
@@ -226,7 +230,7 @@ export default {
const items = accounts
.filter((account) => (
account.transactions.length > 0 || filter.none_zero
account.transactions.length > 0 || !filter.none_zero
))
.map((account) => ({
...pick(account, ['id', 'name', 'code', 'index']),
@@ -248,11 +252,11 @@ export default {
],
opening: {
date: filter.from_date,
amount: opeingBalanceCollection.getClosingBalance(account.id),
amount: formatNumber(opeingBalanceCollection.getClosingBalance(account.id)),
},
closing: {
date: filter.to_date,
amount: closingBalanceCollection.getClosingBalance(account.id),
amount: formatNumber(closingBalanceCollection.getClosingBalance(account.id)),
},
}));
@@ -276,6 +280,8 @@ export default {
.isIn(['year', 'month', 'week', 'day', 'quarter']),
query('number_format.no_cents').optional().isBoolean().toBoolean(),
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
query('account_ids').isArray().optional(),
query('account_ids.*').isNumeric().toInt(),
query('none_zero').optional().isBoolean().toBoolean(),
],
async handler(req, res) {
@@ -298,13 +304,20 @@ export default {
},
none_zero: false,
basis: 'cash',
account_ids: [],
...req.query,
};
if (!Array.isArray(filter.account_ids)) {
filter.account_ids = [filter.account_ids];
}
const balanceSheetTypes = await AccountType.query().where('balance_sheet', true);
// Fetch all balance sheet accounts.
const accounts = await Account.query()
.remember('balance_sheet_accounts')
.whereIn('account_type_id', balanceSheetTypes.map((a) => a.id))
.modify('filterAccounts', filter.account_ids)
.withGraphFetched('type')
.withGraphFetched('transactions')
.modifyGraph('transactions', (builder) => {
@@ -313,6 +326,7 @@ export default {
const journalEntriesCollected = Account.collectJournalEntries(accounts);
const journalEntries = new JournalPoster();
journalEntries.loadEntries(journalEntriesCollected);
// Account balance formmatter based on the given query.
@@ -378,10 +392,12 @@ export default {
accounts: [
{
name: 'Assets',
type: 'assets',
children: [...assets],
},
{
name: 'Liabilities & Equity',
type: 'liabilities_equity',
children: [...liabilitiesEquity],
},
],
@@ -426,6 +442,7 @@ export default {
};
const accounts = await Account.query()
.remember('trial_balance_accounts')
.withGraphFetched('type')
.withGraphFetched('transactions')
.modifyGraph('transactions', (builder) => {
@@ -474,7 +491,7 @@ export default {
query('number_format.no_cents').optional().isBoolean(),
query('number_format.divide_1000').optional().isBoolean(),
query('basis').optional(),
query('none_zero').optional(),
query('none_zero').optional().isBoolean().toBoolean(),
query('display_columns_type').optional().isIn([
'total', 'date_periods',
]),
@@ -507,6 +524,7 @@ export default {
// Fetch all income accounts from storage.
const accounts = await Account.query()
.remember('profit_loss_accounts')
.whereIn('account_type_id', incomeStatementTypes.map((t) => t.id))
.withGraphFetched('type')
.withGraphFetched('transactions');

View File

@@ -45,10 +45,10 @@ export default async (req, res, next) => {
req.knex = knex;
req.organizationId = organizationId;
req.models = {
...Object.values(models).reduce((acc, model) => {
if (model.resource
&& model.resource.default
&& Object.getPrototypeOf(model.resource.default) === TenantModel) {
...Object.values(models).reduce((acc, model) => {
if (typeof model.resource.default.requestModel === 'function' &&
model.resource.default.requestModel() &&
model.name !== 'TenantModel') {
acc[model.name] = model.resource.default.bindKnex(knex);
}
return acc;

View File

@@ -0,0 +1,16 @@
import BaseModel from '@/models/Model';
import CacheService from '@/services/Cache';
export default (Model) => {
return class CachableModel extends Model{
static flushCache(key) {
const modelName = this.name;
if (key) {
CacheService.del(`${modelName}.${key}`);
} else {
CacheService.delStartWith(modelName);
}
}
};
}

View File

@@ -0,0 +1,69 @@
import { QueryBuilder } from 'objection';
import crypto from 'crypto';
import CacheService from '@/services/Cache';
export default class CachableQueryBuilder extends QueryBuilder{
async then(...args) {
// Flush model cache after insert, delete or update transaction.
if (this.isInsert() || this.isDelete() || this.isUpdate()) {
this.modelClass().flushCache();
}
if (this.cacheTag && this.isFind()) {
this.setCacheKey();
return this.getOrStoreCache().then(...args);
} else {
const promise = this.execute();
return promise.then((result) => {
this.setCache(result);
return result;
}).then(...args);
}
}
getOrStoreCache() {
const storeFunction = () => this.execute();
return new Promise((resolve, reject) => {
CacheService.get(this.cacheKey, storeFunction)
.then((result) => { resolve(result); });
});
}
setCache(results) {
CacheService.set(`${this.cacheKey}`, results, this.cacheSeconds);
}
generateCacheKey() {
const knexSql = this.toKnexQuery().toSQL();
const hashedQuery = crypto.createHash('md5').update(knexSql.sql).digest("hex");
return hashedQuery;
}
remember(key, seconds) {
const modelName = this.modelClass().name;
this.cacheSeconds = seconds;
this.cacheTag = (key) ? `${modelName}.${key}` : modelName;
return this;
}
withGraphFetched(relation, settings) {
if (!this.graphAppends) {
this.graphAppends = [relation];
} else {
this.graphAppends.push(relation);
}
return super.withGraphFetched(relation, settings);
}
setCacheKey() {
const hashedQuery = this.generateCacheKey();
const appends = (this.graphAppends || []).join(this.graphAppends, ',');
this.cacheKey = `${this.cacheTag}.${hashedQuery}.${appends}`;
}
}

View File

@@ -1,13 +1,17 @@
/* eslint-disable global-require */
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import { flatten } from 'lodash';
import TenantModel from '@/models/TenantModel';
import {
buildFilterQuery,
buildSortColumnQuery,
} from '@/lib/ViewRolesBuilder';
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
import CachableModel from '@/lib/Cachable/CachableModel';
import DateSession from '@/models/DateSession';
export default class Account extends TenantModel {
export default class Account extends mixin(TenantModel, [CachableModel, DateSession]) {
/**
* Table name
*/
@@ -15,6 +19,13 @@ export default class Account extends TenantModel {
return 'accounts';
}
/**
* Extend query builder model.
*/
static get QueryBuilder() {
return CachableQueryBuilder;
}
/**
* Model modifiers.
*/

View File

@@ -1,6 +1,8 @@
import { Model } from 'objection';
import moment from 'moment';
import TenantModel from '@/models/TenantModel';
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
export default class AccountTransaction extends TenantModel {
/**
@@ -10,6 +12,13 @@ export default class AccountTransaction extends TenantModel {
return 'accounts_transactions';
}
/**
* Extend query builder model.
*/
static get QueryBuilder() {
return CachableQueryBuilder;
}
/**
* Model modifiers.
*/

View File

@@ -0,0 +1,30 @@
import moment from 'moment';
export default (Model) => {
return class DateSession extends Model {
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
$beforeUpdate(opt, context) {
const maybePromise = super.$beforeUpdate(opt, context);
return Promise.resolve(maybePromise).then(() => {
if (DateSession.timestamps[1]) {
this[DateSession.timestamps[1]] = moment().format('YYYY/MM/DD HH:mm:ss');
}
});
}
$beforeInsert(context) {
const maybePromise = super.$beforeInsert(context);
return Promise.resolve(maybePromise).then(() => {
if (DateSession.timestamps[0]) {
this[DateSession.timestamps[0]] = moment().format('YYYY/MM/DD HH:mm:ss');
}
});
}
}
}

View File

@@ -1,9 +1,9 @@
import { mixin } from 'objection';
import TenantModel from '@/models/TenantModel';
import MetableCollection from '@/lib/Metable/MetableCollection';
import definedOptions from '@/data/options';
export default class Option extends mixin(TenantModel, [mixin]) {
export default class Option extends TenantModel {
/**
* Table name.
*/

View File

@@ -1,7 +1,9 @@
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from '@/models/TenantModel';
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
import CachableModel from '@/lib/Cachable/CachableModel';
export default class Resource extends TenantModel {
export default class Resource extends mixin(TenantModel, [CachableModel]) {
/**
* Table name.
*/
@@ -9,6 +11,13 @@ export default class Resource extends TenantModel {
return 'resources';
}
/**
* Extend query builder model.
*/
static get QueryBuilder() {
return CachableQueryBuilder;
}
/**
* Timestamp columns.
*/

View File

@@ -7,4 +7,11 @@ export default class TenantModel extends BaseModel {
}
return super.bindKnex(this.knexBinded);
}
/**
* Allow to embed models to express request.
*/
static requestModel() {
return true;
}
}

View File

@@ -1,7 +1,9 @@
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from '@/models/TenantModel';
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
import CachableModel from '@/lib/Cachable/CachableModel';
export default class View extends TenantModel {
export default class View extends mixin(TenantModel, [CachableModel]) {
/**
* Table name.
*/
@@ -9,6 +11,13 @@ export default class View extends TenantModel {
return 'views';
}
/**
* Extend query builder model.
*/
static get QueryBuilder() {
return CachableQueryBuilder;
}
/**
* Relationship mapping.
*/

View File

@@ -4,6 +4,7 @@ import JournalEntry from '@/services/Accounting/JournalEntry';
import AccountTransaction from '@/models/AccountTransaction';
import AccountBalance from '@/models/AccountBalance';
import {promiseSerial} from '@/utils';
import Account from '../../models/Account';
export default class JournalPoster {
/**
@@ -85,6 +86,8 @@ export default class JournalPoster {
const balanceFindOneOpers = [];
let balanceAccounts = [];
const effectAccountsOpers = [];
balancesList.forEach((balance) => {
const oper = AccountBalance.tenant()
.query().findOne('account_id', balance.account_id);
@@ -113,12 +116,67 @@ export default class JournalPoster {
});
balanceInsertOpers.push(query);
}
const effectedAccountsOper = this.effectAssociatedAccountsBalance(
balance.accountId, amount, 'USD', method,
);
effectAccountsOpers.push(effectedAccountsOper);
});
await Promise.all([
...balanceUpdateOpers, ...balanceInsertOpers,
]);
}
/**
* Effect associated descendants and parent accounts
* of the given account id.
* @param {Number} accountId
* @param {Number} amount
* @param {String} currencyCode
* @param {*} method
*/
async effectAssociatedAccountsBalance(accountId, amount, currencyCode = 'USD', method) {
const accounts = await Account.query().withGraphFetched('balance');
const accountsDecendences = accounts.getDescendants();
const asyncOpers = [];
const accountsInsertBalance = [];
const accountsUpdateBalance = [];
accounts.forEach((account) => {
const accountBalances = account.balance;
const currencyBalance = accountBalances
.find(balance => balance.currencyCode === currencyCode);
if (currencyBalance) {
accountsInsertBalance.push(account.id);
} else {
accountsUpdateBalance.push(account.id);
}
});
accountsInsertBalance.forEach((accountId) => {
const oper = AccountBalance.tenant().query().insert({
account_id: accountId,
amount: method === 'decrement' ? amount * -1 : amount,
currency_code: currencyCode,
});
asyncOpers.push(oper);
});
if (accountsUpdateBalance.length > 0) {
const oper = AccountBalance.tenant().query()
.whereIn('account_id', accountsUpdateBalance);
[method]('amount', Math.abs(amount))
.where('currency_code', currencyCode);
asyncOpers.push(oper);
}
await Promise.all(asyncOpers);
}
/**
* Saves the stacked journal entries to the storage.
*/
@@ -233,9 +291,9 @@ export default class JournalPoster {
result.debit += entry.debit;
if (entry.accountNormal === 'credit') {
result.balance += (entry.credit) ? entry.credit : -1 * entry.debit;
result.balance += entry.credit - entry.debit;
} else if (entry.accountNormal === 'debit') {
result.balance += (entry.debit) ? entry.debit : -1 * entry.credit;
result.balance += entry.debit - entry.credit;
}
});
return result;

View File

@@ -0,0 +1,52 @@
import NodeCache from 'node-cache';
class Cache {
constructor() {
this.cache = new NodeCache({
// stdTTL: 9999999,
// checkperiod: 9999999 * 0.2,
useClones: false,
});
}
get(key, storeFunction) {
const value = this.cache.get(key);
if (value) {
return Promise.resolve(value);
}
return storeFunction().then((result) => {
this.cache.set(key, result);
return result;
});
}
set(key, results) {
this.cache.set(key, results);
}
del(keys) {
this.cache.del(keys);
}
delStartWith(startStr = '') {
if (!startStr) {
return;
}
const keys = this.cache.keys();
for (const key of keys) {
if (key.indexOf(startStr) === 0) {
this.del(key);
}
}
}
flush() {
this.cache.flushAll();
}
}
export default new Cache();

View File

@@ -2,7 +2,7 @@ import { mixin } from 'objection';
import SystemModel from '@/system/models/SystemModel';
import MetableCollection from '@/lib/Metable/MetableCollection';
export default class Option extends mixin(SystemModel, [mixin]) {
export default class Option extends SystemModel {
/**
* Table name.
*/