mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat: Sorting by dynamic query.
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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();
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|
||||||
|
|||||||
44
server/src/lib/DynamicFilter/DynamicFilter.js
Normal file
44
server/src/lib/DynamicFilter/DynamicFilter.js
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
18
server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js
Normal file
18
server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export default class DynamicFilterAbstructor {
|
||||||
|
constructor() {
|
||||||
|
this.filterRoles = [];
|
||||||
|
this.tableName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTableName(tableName) {
|
||||||
|
this.tableName = tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildLogicExpression() {}
|
||||||
|
|
||||||
|
validateFilterRoles() {}
|
||||||
|
|
||||||
|
buildQuery() {}
|
||||||
|
}
|
||||||
31
server/src/lib/DynamicFilter/DynamicFilterSortBy.js
Normal file
31
server/src/lib/DynamicFilter/DynamicFilterSortBy.js
Normal 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());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
46
server/src/lib/DynamicFilter/DynamicFilterViews.js
Normal file
46
server/src/lib/DynamicFilter/DynamicFilterViews.js
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
13
server/src/lib/DynamicFilter/index.js
Normal file
13
server/src/lib/DynamicFilter/index.js
Normal 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,
|
||||||
|
};
|
||||||
44
server/src/lib/ViewRolesBuilder/FilterRolesDynamicFilter.js
Normal file
44
server/src/lib/ViewRolesBuilder/FilterRolesDynamicFilter.js
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.', () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user