diff --git a/server/src/collection/NestedSet/index.js b/server/src/collection/NestedSet/index.js index b49ffcfb8..ad636783c 100644 --- a/server/src/collection/NestedSet/index.js +++ b/server/src/collection/NestedSet/index.js @@ -37,12 +37,12 @@ export default class NestedSet { toTree() { const map = this.linkChildren(); - const tree = {}; + const tree = []; this.items.forEach((item) => { const parentNodeId = item[this.options.parentId]; if (!parentNodeId) { - tree[item.id] = map[item.id]; + tree.push(map[item.id]); } }); this.collection = Object.values(tree); diff --git a/server/src/data/ResourceFieldsKeys.js b/server/src/data/ResourceFieldsKeys.js index e4f3c7d6e..cb0bd5ddc 100644 --- a/server/src/data/ResourceFieldsKeys.js +++ b/server/src/data/ResourceFieldsKeys.js @@ -25,6 +25,7 @@ export default { 'type': { column: 'account_type_id', relation: 'account_types.id', + relationColumn: 'account_types.name', }, 'description': { column: 'description', diff --git a/server/src/http/controllers/Accounts.js b/server/src/http/controllers/Accounts.js index ea0f27ff6..289fc1842 100644 --- a/server/src/http/controllers/Accounts.js +++ b/server/src/http/controllers/Accounts.js @@ -1,20 +1,32 @@ 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 Account from '@/models/Account'; import AccountType from '@/models/AccountType'; import AccountTransaction from '@/models/AccountTransaction'; import JournalPoster from '@/services/Accounting/JournalPoster'; import AccountBalance from '@/models/AccountBalance'; +import NestedSet from '@/collection/NestedSet'; import Resource from '@/models/Resource'; import View from '@/models/View'; import JWTAuth from '@/http/middleware/jwtAuth'; -import NestedSet from '../../collection/NestedSet'; import { mapViewRolesToConditionals, - validateViewRoles, + mapFilterRolesToDynamicFilter, } from '@/lib/ViewRolesBuilder'; -import FilterRoles from '@/lib/FilterRoles'; +import { + DynamicFilter, + DynamicFilterSortBy, + DynamicFilterViews, + DynamicFilterFilterRoles, +} from '@/lib/DynamicFilter'; + export default { /** @@ -40,6 +52,10 @@ export default { this.getAccountsList.validation, asyncMiddleware(this.getAccountsList.handler)); + router.delete('/', + this.deleteBulkAccounts.validation, + asyncMiddleware(this.deleteBulkAccounts.handler)); + router.delete('/:id', this.deleteAccount.validation, asyncMiddleware(this.deleteAccount.handler)); @@ -69,7 +85,7 @@ export default { newAccount: { validation: [ 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('description').optional().trim().escape(), ], @@ -217,6 +233,9 @@ export default { query('custom_view_id').optional().isNumeric().toInt(), query('stringified_filter_roles').optional().isJSON(), + + query('column_sort_order').optional(), + query('sort_order').optional().isIn(['desc', 'asc']), ], async handler(req, res) { const validationErrors = validationResult(req); @@ -226,55 +245,77 @@ export default { code: 'validation_error', ...validationErrors, }); } - const filter = { account_types: [], display_type: 'tree', filter_roles: [], + sort_order: 'asc', ...req.query, }; if (filter.stringified_filter_roles) { filter.filter_roles = JSON.parse(filter.stringified_filter_roles); } const errorReasons = []; - const viewConditionals = []; const accountsResource = await Resource.query() - .where('name', 'accounts').withGraphFetched('fields').first(); + .where('name', 'accounts') + .withGraphFetched('fields') + .first(); if (!accountsResource) { return res.status(400).send({ errors: [{ type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200 }], }); } + const resourceFieldsKeys = accountsResource.fields.map((c) => c.key); + const view = await View.query().onBuild((builder) => { if (filter.custom_view_id) { builder.where('id', filter.custom_view_id); } else { builder.where('favourite', true); } - builder.where('resource_id', accountsResource.id); + // builder.where('resource_id', accountsResource.id); builder.withGraphFetched('roles.field'); builder.withGraphFetched('columns'); builder.first(); }); + const dynamicFilter = new DynamicFilter(Account.tableName); - if (view && view.roles.length > 0) { - viewConditionals.push( - ...mapViewRolesToConditionals(view.roles), - ); - if (!validateViewRoles(viewConditionals, view.rolesLogicExpression)) { - errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 }); + if (filter.column_sort_order) { + if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) { + errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 }); } + const sortByFilter = new DynamicFilterSortBy( + filter.column_sort_order, + filter.sort_order, + ); + dynamicFilter.setFilter(sortByFilter); } - // Validate the accounts resource fields. - const filterRoles = new FilterRoles(Account.tableName, - filter.filter_roles.map((role) => ({ ...role, columnKey: role.fieldKey })), - accountsResource.fields); + // View roles. + if (view && view.roles.length > 0) { + const viewFilter = new DynamicFilterViews( + 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) { - errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 }); + if (filterRoles.validateFilterRoles().length > 0) { + errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 }); + } } if (errorReasons.length > 0) { return res.status(400).send({ errors: errorReasons }); @@ -284,33 +325,14 @@ export default { builder.withGraphFetched('type'); builder.withGraphFetched('balance'); - // Build custom view conditions query. - if (viewConditionals.length > 0) { - builder.modify('viewRolesBuilder', viewConditionals, view.rolesLogicExpression); - } - // Build filter query. - if (filter.filter_roles.length > 0) { - filterRoles.buildQuery()(builder); - } + dynamicFilter.buildQuery()(builder); }); const nestedAccounts = new NestedSet(accounts, { parentId: 'parentAccountId' }); - const groupsAccounts = nestedAccounts.toTree(); - const accountsList = []; + const nestedSetAccounts = nestedAccounts.toTree(); - 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({ - accounts: accountsList, + accounts: nestedSetAccounts, ...(view) ? { customViewId: view.id, } : {}, @@ -424,4 +446,57 @@ export default { // 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(); + }, + }, }; diff --git a/server/src/http/controllers/Views.js b/server/src/http/controllers/Views.js index c10b90bf1..4f0d1f5cb 100644 --- a/server/src/http/controllers/Views.js +++ b/server/src/http/controllers/Views.js @@ -209,7 +209,7 @@ export default { form.roles.forEach((role) => { const fieldModel = resourceFieldsKeysMap.get(role.field_key); - + const saveViewRoleOper = ViewRole.query().insert({ ...pick(role, ['comparator', 'value', 'index']), field_id: fieldModel.id, @@ -245,7 +245,7 @@ export default { 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.*.index').exists().isNumeric().toInt(), diff --git a/server/src/lib/DynamicFilter/DynamicFilter.js b/server/src/lib/DynamicFilter/DynamicFilter.js new file mode 100644 index 000000000..be7df1ee7 --- /dev/null +++ b/server/src/lib/DynamicFilter/DynamicFilter.js @@ -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); + }; + } +} \ No newline at end of file diff --git a/server/src/lib/FilterRoles/index.js b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.js similarity index 69% rename from server/src/lib/FilterRoles/index.js rename to server/src/lib/DynamicFilter/DynamicFilterFilterRoles.js index ee6ce491a..229a83e91 100644 --- a/server/src/lib/FilterRoles/index.js +++ b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.js @@ -1,22 +1,25 @@ import { difference } from 'lodash'; +import DynamicFilterRoleAbstructor from '@/lib/DynamicFilter/DynamicFilterRoleAbstructor'; import { buildFilterQuery, -} from '../ViewRolesBuilder'; +} from '@/lib/ViewRolesBuilder'; -export default class FilterRoles { +export default class FilterRoles extends DynamicFilterRoleAbstructor { /** * Constructor method. * @param {Array} filterRoles - * @param {Array} resourceFields - */ - constructor(tableName, filterRoles, resourceFields) { + constructor(filterRoles, resourceFields) { + super(); + this.filterRoles = filterRoles.map((role, index) => ({ ...role, index: index + 1, columnKey: role.field_key, + comparator: role.comparator === 'AND' ? '&&' : '||', })); this.resourceFields = resourceFields; - this.tableName = tableName; } validateFilterRoles() { @@ -36,10 +39,12 @@ export default class FilterRoles { return expression.trim(); } - // @public + /** + * Builds database query of view roles. + */ buildQuery() { - const logicExpression = this.buildLogicExpression(); return (builder) => { + const logicExpression = this.buildLogicExpression(); buildFilterQuery(this.tableName, this.filterRoles, logicExpression)(builder); }; } diff --git a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js new file mode 100644 index 000000000..e7ee46bd5 --- /dev/null +++ b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js @@ -0,0 +1,18 @@ + + +export default class DynamicFilterAbstructor { + constructor() { + this.filterRoles = []; + this.tableName = ''; + } + + setTableName(tableName) { + this.tableName = tableName; + } + + buildLogicExpression() {} + + validateFilterRoles() {} + + buildQuery() {} +} \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilterSortBy.js b/server/src/lib/DynamicFilter/DynamicFilterSortBy.js new file mode 100644 index 000000000..dbb50ce69 --- /dev/null +++ b/server/src/lib/DynamicFilter/DynamicFilterSortBy.js @@ -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()); + } + }; + } +} diff --git a/server/src/lib/DynamicFilter/DynamicFilterViews.js b/server/src/lib/DynamicFilter/DynamicFilterViews.js new file mode 100644 index 000000000..3fa6e493b --- /dev/null +++ b/server/src/lib/DynamicFilter/DynamicFilterViews.js @@ -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); + }; + } +} \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/index.js b/server/src/lib/DynamicFilter/index.js new file mode 100644 index 000000000..10cc6221c --- /dev/null +++ b/server/src/lib/DynamicFilter/index.js @@ -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, +}; \ No newline at end of file diff --git a/server/src/lib/ViewRolesBuilder/FilterRolesDynamicFilter.js b/server/src/lib/ViewRolesBuilder/FilterRolesDynamicFilter.js new file mode 100644 index 000000000..076a7e378 --- /dev/null +++ b/server/src/lib/ViewRolesBuilder/FilterRolesDynamicFilter.js @@ -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); + }; + } +} \ No newline at end of file diff --git a/server/src/lib/ViewRolesBuilder/index.js b/server/src/lib/ViewRolesBuilder/index.js index e86987317..f8b5fc67a 100644 --- a/server/src/lib/ViewRolesBuilder/index.js +++ b/server/src/lib/ViewRolesBuilder/index.js @@ -29,9 +29,8 @@ export function getRoleFieldColumn(tableName, columnKey) { */ export function buildRoleQuery(tableName, role) { 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) { case 'equals': default: @@ -44,6 +43,7 @@ export function buildRoleQuery(tableName, role) { builder.whereNot(comparatorColumn, role.value); }; case 'contain': + case 'contains': return (builder) => { 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. * @@ -110,7 +121,6 @@ export function buildFilterRolesQuery(tableName, roles, logicExpression = '') { */ export const buildFilterQuery = (tableName, roles, logicExpression) => { return (builder) => { - buildFilterRolesJoins(tableName, roles)(builder); buildFilterRolesQuery(tableName, roles, logicExpression)(builder); }; }; @@ -148,4 +158,28 @@ export function mapViewRolesToConditionals(viewRoles) { slug: viewRole.field.slug, 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); + }; } \ No newline at end of file diff --git a/server/src/models/Account.js b/server/src/models/Account.js index 7fed7f7cd..52f5c1681 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -4,6 +4,7 @@ import { flatten } from 'lodash'; import BaseModel from '@/models/Model'; import { buildFilterQuery, + buildSortColumnQuery, } from '@/lib/ViewRolesBuilder'; export default class Account extends BaseModel { /** @@ -33,6 +34,9 @@ export default class Account extends BaseModel { viewRolesBuilder(query, conditionals, expression) { buildFilterQuery(Account.tableName, conditionals, expression)(query); }, + sortColumnBuilder(query, columnKey, direction) { + buildSortColumnQuery(Account.tableName, columnKey, direction)(query); + } }; } diff --git a/server/tests/collection/NestedSet.test.js b/server/tests/collection/NestedSet.test.js index 5e51dadad..32862bfde 100644 --- a/server/tests/collection/NestedSet.test.js +++ b/server/tests/collection/NestedSet.test.js @@ -4,13 +4,14 @@ import NestedSet from '@/collection/NestedSet'; describe('NestedSet', () => { it('Should link parent and children nodes.', () => { const flattenArray = [ + { id: 10 }, { id: 1 }, { - id: 2, + id: 3, parent_id: 1, }, { - id: 3, + id: 2, parent_id: 1, }, { @@ -18,14 +19,17 @@ describe('NestedSet', () => { parent_id: 3, }, ]; - const collection = new NestedSet(flattenArray); const treeGroups = collection.toTree(); - expect(treeGroups[0].id).equals(1); - expect(treeGroups[0].children[0].id).equals(2); - expect(treeGroups[0].children[1].id).equals(3); - expect(treeGroups[0].children[1].children[0].id).equals(4); + expect(treeGroups[0].id).equals(10); + expect(treeGroups[1].id).equals(1); + + 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.', () => { diff --git a/server/tests/routes/accounts.test.js b/server/tests/routes/accounts.test.js index bb427d036..9acb11299 100644 --- a/server/tests/routes/accounts.test.js +++ b/server/tests/routes/accounts.test.js @@ -94,20 +94,25 @@ describe('routes: /accounts/', () => { it('Should store account data in the storage.', async () => { const account = await create('account'); - await request().post('/api/accounts') + const res = await request().post('/api/accounts') .set('x-access-token', loginRes.body.token) .send({ name: 'Account Name', description: 'desc here', - account_type: account.account_type_id, + account_type_id: account.accountTypeId, 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.account_type_id).equals(account.account_type_id); - expect(accountModel.parent_account_id).equals(account.parent_account_id); + expect(accountModel.accountTypeId).equals(account.accountTypeId); + expect(accountModel.parentAccountId).equals(account.id); }); }); @@ -190,17 +195,17 @@ describe('routes: /accounts/', () => { }); describe('GET: `/accounts`', () => { - it('Should retrieve accounts resource not found.', async () => { - const res = await request() - .get('/api/accounts') - .set('x-access-token', loginRes.body.token) - .send(); + // it('Should retrieve accounts resource not found.', async () => { + // const res = await request() + // .get('/api/accounts') + // .set('x-access-token', loginRes.body.token) + // .send(); - expect(res.status).equals(400); - expect(res.body.errors).include.something.that.deep.equals({ - type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200, - }); - }); + // expect(res.status).equals(400); + // expect(res.body.errors).include.something.that.deep.equals({ + // type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200, + // }); + // }); it('Should retrieve chart of accounts', async () => { await create('resource', { name: 'accounts' }); @@ -213,7 +218,7 @@ describe('routes: /accounts/', () => { .send(); 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 () => { @@ -237,13 +242,15 @@ describe('routes: /accounts/', () => { const accountsView = await create('view', { name: 'Accounts View', 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', { view_id: accountsView.id, index: 1, field_id: accountTypeField.id, - value: '2', + value: accountType.name, comparator: 'equals', }); await create('view_role', { @@ -251,12 +258,12 @@ describe('routes: /accounts/', () => { index: 2, field_id: accountNameField.id, value: 'account', - comparator: 'contain', + comparator: 'contains', }); - await create('account', { name: 'account-1' }); - await create('account', { name: 'account-2' }); - await create('account', { name: 'account-3', account_type_id: 2 }); + await create('account', { name: 'account-1', account_type_id: accountType.id }); + await create('account', { name: 'account-2', account_type_id: accountType.id }); + await create('account', { name: 'account-3' }); const res = await request() .get('/api/accounts') @@ -265,10 +272,10 @@ describe('routes: /accounts/', () => { .send(); expect(res.body.accounts.length).equals(2); - expect(res.body.accounts[0].name).equals('account-2'); - expect(res.body.accounts[1].name).equals('account-3'); - expect(res.body.accounts[0].account_type_id).equals(2); - expect(res.body.accounts[1].account_type_id).equals(2); + expect(res.body.accounts[0].name).equals('account-1'); + expect(res.body.accounts[1].name).equals('account-2'); + expect(res.body.accounts[0].account_type_id).equals(accountType.id); + 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 () => { @@ -286,15 +293,17 @@ describe('routes: /accounts/', () => { resource_id: resource.id, roles_logic_expression: '1', }); + + const accountType = await create('account_type'); const accountsViewRole = await create('view_role', { view_id: accountsView.id, index: 1, field_id: accountTypeField.id, - value: '2', + value: accountType.name, comparator: 'equals', }); - await create('account'); + await create('account', { account_type_id: accountType.id }); await create('account'); await create('account'); @@ -307,7 +316,7 @@ describe('routes: /accounts/', () => { .send(); 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 () => { @@ -325,32 +334,11 @@ describe('routes: /accounts/', () => { expect(res.status).equals(200); - expect(res.body.accounts[0].id).equals(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); - }); + const foundAccount = res.body.accounts.find(a => a.id === account1.id); - it('Should retrieve accounts and child accounts in flat display with dashed accounts name.', async () => { - const resource = await create('resource', { name: 'accounts' }); - - 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}`); + expect(foundAccount.id).equals(account1.id); + expect(foundAccount.children[0].id).equals(account2.id); + expect(foundAccount.children[0].children[0].id).equals(account3.id); }); it('Should retrieve bad request when `filter_roles.*.comparator` not associated to `field_key`.', () => { @@ -370,12 +358,12 @@ describe('routes: /accounts/', () => { .query({ stringified_filter_roles: JSON.stringify([{ condition: 'AND', - field_key: 'name', + field_key: 'not_found', comparator: 'equals', value: 'ahmed', }, { condition: 'AND', - field_key: 'name', + field_key: 'mybe_found', comparator: 'equals', value: 'ahmed', }]), @@ -392,12 +380,20 @@ describe('routes: /accounts/', () => { it('Should retrieve filtered accounts according to the given account type filter condition.', async () => { const resource = await create('resource', { name: 'accounts' }); - const resourceField = await create('resource_field', { + const keyField = await create('resource_field', { key: 'type', 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 account3 = await create('account'); @@ -406,10 +402,15 @@ describe('routes: /accounts/', () => { .set('x-access-token', loginRes.body.token) .query({ stringified_filter_roles: JSON.stringify([{ - condition: 'AND', - field_key: resourceField.key, + condition: '&&', + field_key: 'type', 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, }); + const accountType = await create('account_type', { name: 'type-name' }); + 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 accountsView = await create('view', { @@ -504,7 +507,7 @@ describe('routes: /accounts/', () => { view_id: accountsView.id, field_id: accountTypeField.id, index: 1, - value: '1', + value: 'type-name', comparator: 'equals', }); @@ -515,7 +518,7 @@ describe('routes: /accounts/', () => { custom_view_id: accountsView.id, stringified_filter_roles: JSON.stringify([{ condition: 'AND', - field_key: accountDescriptionField.key, + field_key: 'description', comparator: 'contain', value: 'target', }]), @@ -525,6 +528,64 @@ describe('routes: /accounts/', () => { expect(res.body.accounts[0].name).equals('ahmed-2'); 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`', () => { @@ -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); + }); + }); }); diff --git a/server/tests/routes/views.test.js b/server/tests/routes/views.test.js index 9fa1b1910..233b91542 100644 --- a/server/tests/routes/views.test.js +++ b/server/tests/routes/views.test.js @@ -11,7 +11,7 @@ import ViewColumn from '../../src/models/ViewColumn'; let loginRes; -describe.only('routes: `/views`', () => { +describe('routes: `/views`', () => { beforeEach(async () => { 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 () => { const view = await create('view'); const res = await request() diff --git a/server/tests/services/JournalPoster.test.js b/server/tests/services/JournalPoster.test.js index 16c137135..2da8f87cb 100644 --- a/server/tests/services/JournalPoster.test.js +++ b/server/tests/services/JournalPoster.test.js @@ -288,7 +288,7 @@ describe('JournalPoster', () => { 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 journalEntry1 = new JournalEntry({ id: 1,