feat: Sorting by dynamic query.

This commit is contained in:
Ahmed Bouhuolia
2020-04-18 23:33:41 +02:00
parent c0c4e5af4a
commit cdee562ae7
17 changed files with 566 additions and 130 deletions

View File

@@ -37,12 +37,12 @@ export default class NestedSet {
toTree() { toTree() {
const map = this.linkChildren(); const map = this.linkChildren();
const tree = {}; const tree = [];
this.items.forEach((item) => { this.items.forEach((item) => {
const parentNodeId = item[this.options.parentId]; const parentNodeId = item[this.options.parentId];
if (!parentNodeId) { if (!parentNodeId) {
tree[item.id] = map[item.id]; tree.push(map[item.id]);
} }
}); });
this.collection = Object.values(tree); this.collection = Object.values(tree);

View File

@@ -25,6 +25,7 @@ export default {
'type': { 'type': {
column: 'account_type_id', column: 'account_type_id',
relation: 'account_types.id', relation: 'account_types.id',
relationColumn: 'account_types.name',
}, },
'description': { 'description': {
column: 'description', column: 'description',

View File

@@ -1,20 +1,32 @@
import express from 'express'; import express from 'express';
import { check, validationResult, param, query } from 'express-validator'; import {
check,
validationResult,
param,
query,
} from 'express-validator';
import { difference } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Account from '@/models/Account'; import Account from '@/models/Account';
import AccountType from '@/models/AccountType'; import AccountType from '@/models/AccountType';
import AccountTransaction from '@/models/AccountTransaction'; import AccountTransaction from '@/models/AccountTransaction';
import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalPoster from '@/services/Accounting/JournalPoster';
import AccountBalance from '@/models/AccountBalance'; import AccountBalance from '@/models/AccountBalance';
import NestedSet from '@/collection/NestedSet';
import Resource from '@/models/Resource'; import Resource from '@/models/Resource';
import View from '@/models/View'; import View from '@/models/View';
import JWTAuth from '@/http/middleware/jwtAuth'; import JWTAuth from '@/http/middleware/jwtAuth';
import NestedSet from '../../collection/NestedSet';
import { import {
mapViewRolesToConditionals, mapViewRolesToConditionals,
validateViewRoles, mapFilterRolesToDynamicFilter,
} from '@/lib/ViewRolesBuilder'; } from '@/lib/ViewRolesBuilder';
import FilterRoles from '@/lib/FilterRoles'; import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter';
export default { export default {
/** /**
@@ -40,6 +52,10 @@ export default {
this.getAccountsList.validation, this.getAccountsList.validation,
asyncMiddleware(this.getAccountsList.handler)); asyncMiddleware(this.getAccountsList.handler));
router.delete('/',
this.deleteBulkAccounts.validation,
asyncMiddleware(this.deleteBulkAccounts.handler));
router.delete('/:id', router.delete('/:id',
this.deleteAccount.validation, this.deleteAccount.validation,
asyncMiddleware(this.deleteAccount.handler)); asyncMiddleware(this.deleteAccount.handler));
@@ -69,7 +85,7 @@ export default {
newAccount: { newAccount: {
validation: [ validation: [
check('name').exists().isLength({ min: 3 }).trim().escape(), check('name').exists().isLength({ min: 3 }).trim().escape(),
check('code').exists().isLength({ max: 10 }).trim().escape(), check('code').optional().isLength({ max: 10 }).trim().escape(),
check('account_type_id').exists().isNumeric().toInt(), check('account_type_id').exists().isNumeric().toInt(),
check('description').optional().trim().escape(), check('description').optional().trim().escape(),
], ],
@@ -217,6 +233,9 @@ export default {
query('custom_view_id').optional().isNumeric().toInt(), query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
query('column_sort_order').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
], ],
async handler(req, res) { async handler(req, res) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
@@ -226,55 +245,77 @@ export default {
code: 'validation_error', ...validationErrors, code: 'validation_error', ...validationErrors,
}); });
} }
const filter = { const filter = {
account_types: [], account_types: [],
display_type: 'tree', display_type: 'tree',
filter_roles: [], filter_roles: [],
sort_order: 'asc',
...req.query, ...req.query,
}; };
if (filter.stringified_filter_roles) { if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles); filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
} }
const errorReasons = []; const errorReasons = [];
const viewConditionals = [];
const accountsResource = await Resource.query() const accountsResource = await Resource.query()
.where('name', 'accounts').withGraphFetched('fields').first(); .where('name', 'accounts')
.withGraphFetched('fields')
.first();
if (!accountsResource) { if (!accountsResource) {
return res.status(400).send({ return res.status(400).send({
errors: [{ type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200 }], errors: [{ type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200 }],
}); });
} }
const resourceFieldsKeys = accountsResource.fields.map((c) => c.key);
const view = await View.query().onBuild((builder) => { const view = await View.query().onBuild((builder) => {
if (filter.custom_view_id) { if (filter.custom_view_id) {
builder.where('id', filter.custom_view_id); builder.where('id', filter.custom_view_id);
} else { } else {
builder.where('favourite', true); builder.where('favourite', true);
} }
builder.where('resource_id', accountsResource.id); // builder.where('resource_id', accountsResource.id);
builder.withGraphFetched('roles.field'); builder.withGraphFetched('roles.field');
builder.withGraphFetched('columns'); builder.withGraphFetched('columns');
builder.first(); builder.first();
}); });
const dynamicFilter = new DynamicFilter(Account.tableName);
if (view && view.roles.length > 0) { if (filter.column_sort_order) {
viewConditionals.push( if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) {
...mapViewRolesToConditionals(view.roles), errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
);
if (!validateViewRoles(viewConditionals, view.rolesLogicExpression)) {
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
} }
const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_order,
filter.sort_order,
);
dynamicFilter.setFilter(sortByFilter);
} }
// Validate the accounts resource fields. // View roles.
const filterRoles = new FilterRoles(Account.tableName, if (view && view.roles.length > 0) {
filter.filter_roles.map((role) => ({ ...role, columnKey: role.fieldKey })), const viewFilter = new DynamicFilterViews(
accountsResource.fields); mapViewRolesToConditionals(view.roles),
view.rolesLogicExpression,
);
if (!viewFilter.validateFilterRoles()) {
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
}
dynamicFilter.setFilter(viewFilter);
}
// Filter roles.
if (filter.filter_roles.length > 0) {
// Validate the accounts resource fields.
const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles),
accountsResource.fields,
);
dynamicFilter.setFilter(filterRoles);
if (filterRoles.validateFilterRoles().length > 0) { if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 }); errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 });
}
} }
if (errorReasons.length > 0) { if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons }); return res.status(400).send({ errors: errorReasons });
@@ -284,33 +325,14 @@ export default {
builder.withGraphFetched('type'); builder.withGraphFetched('type');
builder.withGraphFetched('balance'); builder.withGraphFetched('balance');
// Build custom view conditions query. dynamicFilter.buildQuery()(builder);
if (viewConditionals.length > 0) {
builder.modify('viewRolesBuilder', viewConditionals, view.rolesLogicExpression);
}
// Build filter query.
if (filter.filter_roles.length > 0) {
filterRoles.buildQuery()(builder);
}
}); });
const nestedAccounts = new NestedSet(accounts, { parentId: 'parentAccountId' }); const nestedAccounts = new NestedSet(accounts, { parentId: 'parentAccountId' });
const groupsAccounts = nestedAccounts.toTree(); const nestedSetAccounts = nestedAccounts.toTree();
const accountsList = [];
if (filter.display_type === 'tree') {
accountsList.push(...groupsAccounts);
} else if (filter.display_type === 'flat') {
const flattenAccounts = nestedAccounts.flattenTree((account, parentAccount) => {
if (parentAccount) {
account.name = `${parentAccount.name}${account.name}`;
}
return account;
});
accountsList.push(...flattenAccounts);
}
return res.status(200).send({ return res.status(200).send({
accounts: accountsList, accounts: nestedSetAccounts,
...(view) ? { ...(view) ? {
customViewId: view.id, customViewId: view.id,
} : {}, } : {},
@@ -424,4 +446,57 @@ export default {
// return res.status(200).send(); // return res.status(200).send();
}, },
}, },
deleteBulkAccounts: {
validation: [
query('ids').isArray(),
query('ids.*').isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const filter = { ids: [], ...req.query };
const accounts = await Account.query().onBuild((builder) => {
if (filter.ids.length) {
builder.whereIn('id', filter.ids);
}
});
const accountsIds = accounts.map(a => a.id);
const notFoundAccounts = difference(filter.ids, accountsIds);
if (notFoundAccounts.length > 0) {
return res.status(404).send({
errors: [{ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200, ids: notFoundAccounts }],
});
}
const accountsTransactions = await AccountTransaction.query()
.whereIn('account_id', accountsIds)
.count('id as transactions_count')
.groupBy('account_id')
.select('account_id');
const accountsHasTransactions = [];
accountsTransactions.forEach((transaction) => {
if (transaction.transactionsCount > 0) {
accountsHasTransactions.push(transaction.accountId);
}
});
if (accountsHasTransactions.length > 0) {
return res.status(400).send({
errors: [{ type: 'ACCOUNTS.HAS.TRANSACTIONS', code: 300, ids: accountsHasTransactions }],
});
}
await Account.query()
.whereIn('id', accounts.map((a) => a.id))
.delete();
return res.status(200).send();
},
},
}; };

View File

@@ -209,7 +209,7 @@ export default {
form.roles.forEach((role) => { form.roles.forEach((role) => {
const fieldModel = resourceFieldsKeysMap.get(role.field_key); const fieldModel = resourceFieldsKeysMap.get(role.field_key);
const saveViewRoleOper = ViewRole.query().insert({ const saveViewRoleOper = ViewRole.query().insert({
...pick(role, ['comparator', 'value', 'index']), ...pick(role, ['comparator', 'value', 'index']),
field_id: fieldModel.id, field_id: fieldModel.id,
@@ -245,7 +245,7 @@ export default {
check('columns').exists().isArray({ min: 1 }), check('columns').exists().isArray({ min: 1 }),
check('columns.*.id').optional().isNumeric().toInt(), check('columns.*.id').optional().isNumeric().toInt(),
check('columns.*.key').exists().escape().trim(), check('columns.*.key').exists().escape().trim(),
check('columns.*.index').exists().isNumeric().toInt(), check('columns.*.index').exists().isNumeric().toInt(),

View File

@@ -0,0 +1,44 @@
import { uniqBy } from 'lodash';
import {
buildFilterRolesJoins,
} from '@/lib/ViewRolesBuilder';
export default class DynamicFilter {
/**
* Constructor.
* @param {String} tableName -
*/
constructor(tableName) {
this.tableName = tableName;
this.filters = [];
}
/**
* Set filter.
* @param {*} filterRole -
*/
setFilter(filterRole) {
filterRole.setTableName(this.tableName);
this.filters.push(filterRole);
}
/**
* Builds queries of filter roles.
*/
buildQuery() {
const buildersCallbacks = [];
const tableColumns = [];
this.filters.forEach((filter) => {
const { filterRoles } = filter;
buildersCallbacks.push(filter.buildQuery());
tableColumns.push(...(Array.isArray(filterRoles)) ? filterRoles : [filterRoles]);
});
return (builder) => {
buildersCallbacks.forEach((builderCallback) => {
builderCallback(builder);
});
buildFilterRolesJoins(this.tableName, uniqBy(tableColumns, 'columnKey'))(builder);
};
}
}

View File

@@ -1,22 +1,25 @@
import { difference } from 'lodash'; import { difference } from 'lodash';
import DynamicFilterRoleAbstructor from '@/lib/DynamicFilter/DynamicFilterRoleAbstructor';
import { import {
buildFilterQuery, buildFilterQuery,
} from '../ViewRolesBuilder'; } from '@/lib/ViewRolesBuilder';
export default class FilterRoles { export default class FilterRoles extends DynamicFilterRoleAbstructor {
/** /**
* Constructor method. * Constructor method.
* @param {Array} filterRoles - * @param {Array} filterRoles -
* @param {Array} resourceFields - * @param {Array} resourceFields -
*/ */
constructor(tableName, filterRoles, resourceFields) { constructor(filterRoles, resourceFields) {
super();
this.filterRoles = filterRoles.map((role, index) => ({ this.filterRoles = filterRoles.map((role, index) => ({
...role, ...role,
index: index + 1, index: index + 1,
columnKey: role.field_key, columnKey: role.field_key,
comparator: role.comparator === 'AND' ? '&&' : '||',
})); }));
this.resourceFields = resourceFields; this.resourceFields = resourceFields;
this.tableName = tableName;
} }
validateFilterRoles() { validateFilterRoles() {
@@ -36,10 +39,12 @@ export default class FilterRoles {
return expression.trim(); return expression.trim();
} }
// @public /**
* Builds database query of view roles.
*/
buildQuery() { buildQuery() {
const logicExpression = this.buildLogicExpression();
return (builder) => { return (builder) => {
const logicExpression = this.buildLogicExpression();
buildFilterQuery(this.tableName, this.filterRoles, logicExpression)(builder); buildFilterQuery(this.tableName, this.filterRoles, logicExpression)(builder);
}; };
} }

View File

@@ -0,0 +1,18 @@
export default class DynamicFilterAbstructor {
constructor() {
this.filterRoles = [];
this.tableName = '';
}
setTableName(tableName) {
this.tableName = tableName;
}
buildLogicExpression() {}
validateFilterRoles() {}
buildQuery() {}
}

View File

@@ -0,0 +1,31 @@
import DynamicFilterRoleAbstructor from '@/lib/DynamicFilter/DynamicFilterRoleAbstructor';
import {
getRoleFieldColumn,
} from '@/lib/ViewRolesBuilder';
export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
constructor(sortByFieldKey, sortDirection) {
super();
this.filterRoles = {
columnKey: sortByFieldKey,
value: sortDirection,
comparator: 'sort_by',
};
}
/**
* Builds database query of sort by column on the given direction.
*/
buildQuery() {
const { columnKey = null, value = null } = this.filterRoles;
return (builder) => {
const fieldRelation = getRoleFieldColumn(this.tableName, columnKey);
if (columnKey) {
builder.orderBy(`${this.tableName}.${fieldRelation.column}`, value.toLowerCase());
}
};
}
}

View File

@@ -0,0 +1,46 @@
import DynamicFilterRoleAbstructor from '@/lib/DynamicFilter/DynamicFilterRoleAbstructor';
import {
validateViewRoles,
buildFilterQuery,
} from '@/lib/ViewRolesBuilder';
export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
/**
* Constructor method.
* @param {*} filterRoles -
* @param {*} logicExpression -
*/
constructor(filterRoles, logicExpression) {
super();
this.filterRoles = filterRoles;
this.logicExpression = logicExpression
.replace('AND', '&&')
.replace('OR', '||');
this.tableName = '';
}
/**
* Retrieve logic expression.
*/
buildLogicExpression() {
return this.logicExpression;
}
/**
* Validates filter roles.
*/
validateFilterRoles() {
return validateViewRoles(this.filterRoles, this.logicExpression);
}
/**
* Builds database query of view roles.
*/
buildQuery() {
return (builder) => {
buildFilterQuery(this.tableName, this.filterRoles, this.logicExpression)(builder);
};
}
}

View File

@@ -0,0 +1,13 @@
import DynamicFilter from './DynamicFilter';
import DynamicFilterSortBy from './DynamicFilterSortBy';
import DynamicFilterViews from './DynamicFilterViews';
import DynamicFilterFilterRoles from './DynamicFilterFilterRoles';
export {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
};

View File

@@ -0,0 +1,44 @@
import DynamicFilterRoleAbstructor from '@/lib/DynamicFilter/DynamicFilterRoleAbstructor';
import {
validateViewRoles,
buildFilterQuery,
} from '@/lib/ViewRolesBuilder';
export default class ViewRolesDynamicFilter extends DynamicFilterRoleAbstructor {
/**
* Constructor method.
* @param {*} filterRoles
* @param {*} logicExpression
*/
constructor(filterRoles, logicExpression) {
super();
this.filterRoles = filterRoles;
this.logicExpression = logicExpression;
this.tableName = '';
}
/**
* Retrieve logic expression.
*/
buildLogicExpression() {
return this.logicExpression;
}
/**
* Validates filter roles.
*/
validateFilterRoles() {
return validateViewRoles(this.filterRoles, this.logicExpression);
}
/**
* Builds database query of view roles.
*/
buildQuery() {
return (builder) => {
buildFilterQuery(this.tableName, this.filterRoles, this.logicExpression)(builder);
};
}
}

View File

@@ -29,9 +29,8 @@ export function getRoleFieldColumn(tableName, columnKey) {
*/ */
export function buildRoleQuery(tableName, role) { export function buildRoleQuery(tableName, role) {
const fieldRelation = getRoleFieldColumn(tableName, role.columnKey); const fieldRelation = getRoleFieldColumn(tableName, role.columnKey);
const comparatorColumn = fieldRelation.relation || `${tableName}.${fieldRelation.column}`; const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`;
console.log(comparatorColumn, role.value);
switch (role.comparator) { switch (role.comparator) {
case 'equals': case 'equals':
default: default:
@@ -44,6 +43,7 @@ export function buildRoleQuery(tableName, role) {
builder.whereNot(comparatorColumn, role.value); builder.whereNot(comparatorColumn, role.value);
}; };
case 'contain': case 'contain':
case 'contains':
return (builder) => { return (builder) => {
builder.where(comparatorColumn, 'LIKE', `%${role.value}%`); builder.where(comparatorColumn, 'LIKE', `%${role.value}%`);
}; };
@@ -78,6 +78,17 @@ export function buildFilterRolesJoins(tableName, roles) {
}; };
} }
export function buildSortColumnJoin(tableName, sortColumnKey) {
return (builder) => {
const fieldColumn = getRoleFieldColumn(tableName, sortColumnKey);
if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(joinTable, `${tableName}.${fieldColumn.column}`, '=', fieldColumn.relation);
}
}
}
/** /**
* Builds database query from stored view roles. * Builds database query from stored view roles.
* *
@@ -110,7 +121,6 @@ export function buildFilterRolesQuery(tableName, roles, logicExpression = '') {
*/ */
export const buildFilterQuery = (tableName, roles, logicExpression) => { export const buildFilterQuery = (tableName, roles, logicExpression) => {
return (builder) => { return (builder) => {
buildFilterRolesJoins(tableName, roles)(builder);
buildFilterRolesQuery(tableName, roles, logicExpression)(builder); buildFilterRolesQuery(tableName, roles, logicExpression)(builder);
}; };
}; };
@@ -148,4 +158,28 @@ export function mapViewRolesToConditionals(viewRoles) {
slug: viewRole.field.slug, slug: viewRole.field.slug,
index: viewRole.index, index: viewRole.index,
})); }));
}
export function mapFilterRolesToDynamicFilter(roles) {
return roles.map((role) => ({
...role,
columnKey: role.fieldKey,
}));
}
/**
* Builds sort column query.
* @param {String} tableName -
* @param {String} columnKey -
* @param {String} sortDirection -
*/
export function buildSortColumnQuery(tableName, columnKey, sortDirection) {
const fieldRelation = getRoleFieldColumn(tableName, columnKey);
const sortColumn = fieldRelation.relation || `${tableName}.${fieldRelation.column}`;
return (builder) => {
builder.orderBy(sortColumn, sortDirection);
buildSortColumnJoin(tableName, columnKey)(builder);
};
} }

View File

@@ -4,6 +4,7 @@ import { flatten } from 'lodash';
import BaseModel from '@/models/Model'; import BaseModel from '@/models/Model';
import { import {
buildFilterQuery, buildFilterQuery,
buildSortColumnQuery,
} from '@/lib/ViewRolesBuilder'; } from '@/lib/ViewRolesBuilder';
export default class Account extends BaseModel { export default class Account extends BaseModel {
/** /**
@@ -33,6 +34,9 @@ export default class Account extends BaseModel {
viewRolesBuilder(query, conditionals, expression) { viewRolesBuilder(query, conditionals, expression) {
buildFilterQuery(Account.tableName, conditionals, expression)(query); buildFilterQuery(Account.tableName, conditionals, expression)(query);
}, },
sortColumnBuilder(query, columnKey, direction) {
buildSortColumnQuery(Account.tableName, columnKey, direction)(query);
}
}; };
} }

View File

@@ -4,13 +4,14 @@ import NestedSet from '@/collection/NestedSet';
describe('NestedSet', () => { describe('NestedSet', () => {
it('Should link parent and children nodes.', () => { it('Should link parent and children nodes.', () => {
const flattenArray = [ const flattenArray = [
{ id: 10 },
{ id: 1 }, { id: 1 },
{ {
id: 2, id: 3,
parent_id: 1, parent_id: 1,
}, },
{ {
id: 3, id: 2,
parent_id: 1, parent_id: 1,
}, },
{ {
@@ -18,14 +19,17 @@ describe('NestedSet', () => {
parent_id: 3, parent_id: 3,
}, },
]; ];
const collection = new NestedSet(flattenArray); const collection = new NestedSet(flattenArray);
const treeGroups = collection.toTree(); const treeGroups = collection.toTree();
expect(treeGroups[0].id).equals(1); expect(treeGroups[0].id).equals(10);
expect(treeGroups[0].children[0].id).equals(2); expect(treeGroups[1].id).equals(1);
expect(treeGroups[0].children[1].id).equals(3);
expect(treeGroups[0].children[1].children[0].id).equals(4); expect(treeGroups[1].children.length).equals(2);
expect(treeGroups[1].children[0].id).equals(3);
expect(treeGroups[1].children[1].id).equals(2);
expect(treeGroups[1].children[0].children[0].id).equals(4);
}); });
it('Should flatten the nested set collection.', () => { it('Should flatten the nested set collection.', () => {

View File

@@ -94,20 +94,25 @@ describe('routes: /accounts/', () => {
it('Should store account data in the storage.', async () => { it('Should store account data in the storage.', async () => {
const account = await create('account'); const account = await create('account');
await request().post('/api/accounts') const res = await request().post('/api/accounts')
.set('x-access-token', loginRes.body.token) .set('x-access-token', loginRes.body.token)
.send({ .send({
name: 'Account Name', name: 'Account Name',
description: 'desc here', description: 'desc here',
account_type: account.account_type_id, account_type_id: account.accountTypeId,
parent_account_id: account.id, parent_account_id: account.id,
}); });
const accountModel = await Account.query().where('name', 'Account Name');
const accountModel = await Account.query()
.where('name', 'Account Name')
.first();
expect(accountModel).a.an('object');
expect(accountModel.description).equals('desc here'); expect(accountModel.description).equals('desc here');
expect(accountModel.account_type_id).equals(account.account_type_id); expect(accountModel.accountTypeId).equals(account.accountTypeId);
expect(accountModel.parent_account_id).equals(account.parent_account_id); expect(accountModel.parentAccountId).equals(account.id);
}); });
}); });
@@ -190,17 +195,17 @@ describe('routes: /accounts/', () => {
}); });
describe('GET: `/accounts`', () => { describe('GET: `/accounts`', () => {
it('Should retrieve accounts resource not found.', async () => { // it('Should retrieve accounts resource not found.', async () => {
const res = await request() // const res = await request()
.get('/api/accounts') // .get('/api/accounts')
.set('x-access-token', loginRes.body.token) // .set('x-access-token', loginRes.body.token)
.send(); // .send();
expect(res.status).equals(400); // expect(res.status).equals(400);
expect(res.body.errors).include.something.that.deep.equals({ // expect(res.body.errors).include.something.that.deep.equals({
type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200, // type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200,
}); // });
}); // });
it('Should retrieve chart of accounts', async () => { it('Should retrieve chart of accounts', async () => {
await create('resource', { name: 'accounts' }); await create('resource', { name: 'accounts' });
@@ -213,7 +218,7 @@ describe('routes: /accounts/', () => {
.send(); .send();
expect(res.status).equals(200); expect(res.status).equals(200);
expect(res.body.accounts.length).equals(1); expect(res.body.accounts.length).above(0);
}); });
it('Should retrieve accounts based on view roles conditionals of the custom view.', async () => { it('Should retrieve accounts based on view roles conditionals of the custom view.', async () => {
@@ -237,13 +242,15 @@ describe('routes: /accounts/', () => {
const accountsView = await create('view', { const accountsView = await create('view', {
name: 'Accounts View', name: 'Accounts View',
resource_id: resource.id, resource_id: resource.id,
roles_logic_expression: '1 && 2', roles_logic_expression: '1 AND 2',
}); });
const accountType = await create('account_type');
await create('view_role', { await create('view_role', {
view_id: accountsView.id, view_id: accountsView.id,
index: 1, index: 1,
field_id: accountTypeField.id, field_id: accountTypeField.id,
value: '2', value: accountType.name,
comparator: 'equals', comparator: 'equals',
}); });
await create('view_role', { await create('view_role', {
@@ -251,12 +258,12 @@ describe('routes: /accounts/', () => {
index: 2, index: 2,
field_id: accountNameField.id, field_id: accountNameField.id,
value: 'account', value: 'account',
comparator: 'contain', comparator: 'contains',
}); });
await create('account', { name: 'account-1' }); await create('account', { name: 'account-1', account_type_id: accountType.id });
await create('account', { name: 'account-2' }); await create('account', { name: 'account-2', account_type_id: accountType.id });
await create('account', { name: 'account-3', account_type_id: 2 }); await create('account', { name: 'account-3' });
const res = await request() const res = await request()
.get('/api/accounts') .get('/api/accounts')
@@ -265,10 +272,10 @@ describe('routes: /accounts/', () => {
.send(); .send();
expect(res.body.accounts.length).equals(2); expect(res.body.accounts.length).equals(2);
expect(res.body.accounts[0].name).equals('account-2'); expect(res.body.accounts[0].name).equals('account-1');
expect(res.body.accounts[1].name).equals('account-3'); expect(res.body.accounts[1].name).equals('account-2');
expect(res.body.accounts[0].account_type_id).equals(2); expect(res.body.accounts[0].account_type_id).equals(accountType.id);
expect(res.body.accounts[1].account_type_id).equals(2); expect(res.body.accounts[1].account_type_id).equals(accountType.id);
}); });
it('Should retrieve accounts based on view roles conditionals with relation join column.', async () => { it('Should retrieve accounts based on view roles conditionals with relation join column.', async () => {
@@ -286,15 +293,17 @@ describe('routes: /accounts/', () => {
resource_id: resource.id, resource_id: resource.id,
roles_logic_expression: '1', roles_logic_expression: '1',
}); });
const accountType = await create('account_type');
const accountsViewRole = await create('view_role', { const accountsViewRole = await create('view_role', {
view_id: accountsView.id, view_id: accountsView.id,
index: 1, index: 1,
field_id: accountTypeField.id, field_id: accountTypeField.id,
value: '2', value: accountType.name,
comparator: 'equals', comparator: 'equals',
}); });
await create('account'); await create('account', { account_type_id: accountType.id });
await create('account'); await create('account');
await create('account'); await create('account');
@@ -307,7 +316,7 @@ describe('routes: /accounts/', () => {
.send(); .send();
expect(res.body.accounts.length).equals(1); expect(res.body.accounts.length).equals(1);
expect(res.body.accounts[0].account_type_id).equals(2); expect(res.body.accounts[0].account_type_id).equals(accountType.id);
}); });
it('Should retrieve accounts and child accounts in nested set graph.', async () => { it('Should retrieve accounts and child accounts in nested set graph.', async () => {
@@ -325,32 +334,11 @@ describe('routes: /accounts/', () => {
expect(res.status).equals(200); expect(res.status).equals(200);
expect(res.body.accounts[0].id).equals(account1.id); const foundAccount = res.body.accounts.find(a => a.id === account1.id);
expect(res.body.accounts[0].children[0].id).equals(account2.id);
expect(res.body.accounts[0].children[0].children[0].id).equals(account3.id);
});
it('Should retrieve accounts and child accounts in flat display with dashed accounts name.', async () => { expect(foundAccount.id).equals(account1.id);
const resource = await create('resource', { name: 'accounts' }); expect(foundAccount.children[0].id).equals(account2.id);
expect(foundAccount.children[0].children[0].id).equals(account3.id);
const account1 = await create('account');
const account2 = await create('account', { parent_account_id: account1.id });
const account3 = await create('account', { parent_account_id: account2.id });
const res = await request()
.get('/api/accounts')
.set('x-access-token', loginRes.body.token)
.query({ display_type: 'flat' })
.send();
expect(res.body.accounts[0].id).equals(account1.id);
expect(res.body.accounts[0].name).equals(account1.name);
expect(res.body.accounts[1].id).equals(account2.id);
expect(res.body.accounts[1].name).equals(`${account1.name}${account2.name}`);
expect(res.body.accounts[2].id).equals(account3.id);
expect(res.body.accounts[2].name).equals(`${account1.name}${account2.name}${account3.name}`);
}); });
it('Should retrieve bad request when `filter_roles.*.comparator` not associated to `field_key`.', () => { it('Should retrieve bad request when `filter_roles.*.comparator` not associated to `field_key`.', () => {
@@ -370,12 +358,12 @@ describe('routes: /accounts/', () => {
.query({ .query({
stringified_filter_roles: JSON.stringify([{ stringified_filter_roles: JSON.stringify([{
condition: 'AND', condition: 'AND',
field_key: 'name', field_key: 'not_found',
comparator: 'equals', comparator: 'equals',
value: 'ahmed', value: 'ahmed',
}, { }, {
condition: 'AND', condition: 'AND',
field_key: 'name', field_key: 'mybe_found',
comparator: 'equals', comparator: 'equals',
value: 'ahmed', value: 'ahmed',
}]), }]),
@@ -392,12 +380,20 @@ describe('routes: /accounts/', () => {
it('Should retrieve filtered accounts according to the given account type filter condition.', async () => { it('Should retrieve filtered accounts according to the given account type filter condition.', async () => {
const resource = await create('resource', { name: 'accounts' }); const resource = await create('resource', { name: 'accounts' });
const resourceField = await create('resource_field', { const keyField = await create('resource_field', {
key: 'type', key: 'type',
resource_id: resource.id, resource_id: resource.id,
}); });
const nameFiled = await create('resource_field', {
key: 'name',
resource_id: resource.id,
});
const accountType = await create('account_type');
const account1 = await create('account', { name: 'ahmed' }); const account1 = await create('account', {
name: 'ahmed',
account_type_id: accountType.id
});
const account2 = await create('account'); const account2 = await create('account');
const account3 = await create('account'); const account3 = await create('account');
@@ -406,10 +402,15 @@ describe('routes: /accounts/', () => {
.set('x-access-token', loginRes.body.token) .set('x-access-token', loginRes.body.token)
.query({ .query({
stringified_filter_roles: JSON.stringify([{ stringified_filter_roles: JSON.stringify([{
condition: 'AND', condition: '&&',
field_key: resourceField.key, field_key: 'type',
comparator: 'equals', comparator: 'equals',
value: '1', value: accountType.name,
}, {
condition: '&&',
field_key: 'name',
comparator: 'equals',
value: 'ahmed',
}]), }]),
}); });
@@ -491,8 +492,10 @@ describe('routes: /accounts/', () => {
key: 'description', resource_id: resource.id, key: 'description', resource_id: resource.id,
}); });
const accountType = await create('account_type', { name: 'type-name' });
const account1 = await create('account', { name: 'ahmed-1' }); const account1 = await create('account', { name: 'ahmed-1' });
const account2 = await create('account', { name: 'ahmed-2', account_type_id: 1, description: 'target' }); const account2 = await create('account', { name: 'ahmed-2', account_type_id: accountType.id, description: 'target' });
const account3 = await create('account', { name: 'ahmed-3' }); const account3 = await create('account', { name: 'ahmed-3' });
const accountsView = await create('view', { const accountsView = await create('view', {
@@ -504,7 +507,7 @@ describe('routes: /accounts/', () => {
view_id: accountsView.id, view_id: accountsView.id,
field_id: accountTypeField.id, field_id: accountTypeField.id,
index: 1, index: 1,
value: '1', value: 'type-name',
comparator: 'equals', comparator: 'equals',
}); });
@@ -515,7 +518,7 @@ describe('routes: /accounts/', () => {
custom_view_id: accountsView.id, custom_view_id: accountsView.id,
stringified_filter_roles: JSON.stringify([{ stringified_filter_roles: JSON.stringify([{
condition: 'AND', condition: 'AND',
field_key: accountDescriptionField.key, field_key: 'description',
comparator: 'contain', comparator: 'contain',
value: 'target', value: 'target',
}]), }]),
@@ -525,6 +528,64 @@ describe('routes: /accounts/', () => {
expect(res.body.accounts[0].name).equals('ahmed-2'); expect(res.body.accounts[0].name).equals('ahmed-2');
expect(res.body.accounts[0].description).equals('target'); expect(res.body.accounts[0].description).equals('target');
}); });
it('Should validate the given `column_sort_order` column on the accounts resource.', async () => {
const resource = await create('resource', { name: 'accounts' });
const res = await request()
.get('/api/accounts')
.set('x-access-token', loginRes.body.token)
.query({
column_sort_order: 'not_found',
sort_order: 'desc',
});
expect(res.body.errors).include.something.that.deep.equals({
type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300,
});
});
it('Should sorting the given `column_sort_order` column on asc direction,', async () => {
const resource = await create('resource', { name: 'accounts' });
const resourceField = await create('resource_field', {
key: 'name', resource_id: resource.id,
});
const accounts1 = await create('account', { name: 'A' });
const accounts2 = await create('account', { name: 'B' });
const res = await request()
.get('/api/accounts')
.set('x-access-token', loginRes.body.token)
.query({
column_sort_order: 'name',
sort_order: 'asc',
});
const AAccountIndex = res.body.accounts.findIndex(a => a.name === 'B');
const BAccountIndex = res.body.accounts.findIndex(a => a.name === 'A');
expect(AAccountIndex).above(BAccountIndex);
});
it('Should sorting the given `column_sort_order` columnw with relation on another table on asc direction.', async () => {
const resource = await create('resource', { name: 'accounts' });
const resourceField = await create('resource_field', {
key: 'type', resource_id: resource.id,
});
const accounts1 = await create('account', { name: 'A' });
const accounts2 = await create('account', { name: 'B' });
const res = await request()
.get('/api/accounts')
.set('x-access-token', loginRes.body.token)
.query({
column_sort_order: 'name',
sort_order: 'asc',
});
expect(res.body.accounts[0].name).equals('A');
expect(res.body.accounts[1].name).equals('B');
});
}); });
describe('DELETE: `/accounts`', () => { describe('DELETE: `/accounts`', () => {
@@ -562,4 +623,60 @@ describe('routes: /accounts/', () => {
}); });
}); });
}); });
describe('DELETE: `/accounts?ids=`', () => {
it('Should response in case on of accounts ids was not exists.', async () => {
const res = await request()
.delete('/api/accounts')
.set('x-access-token', loginRes.body.token)
.query({
ids: [100, 200],
})
.send();
expect(res.status).equals(404);
expect(res.body.errors).include.something.that.deep.equals({
type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200, ids: [100, 200],
});
});
it('Should response bad request in case one of accounts has transactions.', async () => {
const accountTransaction = await create('account_transaction');
const accountTransaction2 = await create('account_transaction');
const res = await request()
.delete('/api/accounts')
.set('x-access-token', loginRes.body.token)
.query({
ids: [accountTransaction.accountId, accountTransaction2.accountId],
})
.send();
expect(res.body.errors).include.something.that.deep.equals({
type: 'ACCOUNTS.HAS.TRANSACTIONS',
code: 300,
ids: [accountTransaction.accountId, accountTransaction2.accountId],
});
});
it('Should delete the given accounts from the storage.', async () => {
const account1 = await create('account');
const account2 = await create('account');
const res = await request()
.delete('/api/accounts')
.set('x-access-token', loginRes.body.token)
.query({
ids: [account1.id, account2.id],
})
.send();
expect(res.status).equals(200);
const foundAccounts = await Account.query()
.whereIn('id', [account1.id, account2.id]);
expect(foundAccounts.length).equals(0);
});
});
}); });

View File

@@ -11,7 +11,7 @@ import ViewColumn from '../../src/models/ViewColumn';
let loginRes; let loginRes;
describe.only('routes: `/views`', () => { describe('routes: `/views`', () => {
beforeEach(async () => { beforeEach(async () => {
loginRes = await login(); loginRes = await login();
}); });
@@ -463,7 +463,7 @@ describe.only('routes: `/views`', () => {
}); });
describe.only('POST: `/views/:view_id`', () => { describe('POST: `/views/:view_id`', () => {
it('Should `name` be required.', async () => { it('Should `name` be required.', async () => {
const view = await create('view'); const view = await create('view');
const res = await request() const res = await request()

View File

@@ -288,7 +288,7 @@ describe('JournalPoster', () => {
expect(journalPoster.deletedEntriesIds[1]).equals(2); expect(journalPoster.deletedEntriesIds[1]).equals(2);
}); });
it.only('Should revert the account balance after remove the entries.', () => { it('Should revert the account balance after remove the entries.', () => {
const journalPoster = new JournalPoster(); const journalPoster = new JournalPoster();
const journalEntry1 = new JournalEntry({ const journalEntry1 = new JournalEntry({
id: 1, id: 1,