mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
WIP
This commit is contained in:
@@ -12,9 +12,6 @@ export default class NestedSet {
|
||||
};
|
||||
this.items = items;
|
||||
this.collection = {};
|
||||
this.toTree();
|
||||
|
||||
return this.collection;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,25 +46,32 @@ export default class NestedSet {
|
||||
}
|
||||
});
|
||||
this.collection = Object.values(tree);
|
||||
return this.collection;
|
||||
}
|
||||
|
||||
walk() {
|
||||
|
||||
getTree() {
|
||||
return this.collection;
|
||||
}
|
||||
|
||||
getParents() {
|
||||
flattenTree(nodeMapper) {
|
||||
const flattenTree = [];
|
||||
|
||||
}
|
||||
const traversal = (nodes, parentNode) => {
|
||||
nodes.forEach((node) => {
|
||||
let nodeMapped = node;
|
||||
|
||||
getChildren() {
|
||||
|
||||
}
|
||||
if (typeof nodeMapper === 'function') {
|
||||
nodeMapped = nodeMapper(nodeMapped, parentNode);
|
||||
}
|
||||
flattenTree.push(nodeMapped);
|
||||
|
||||
toFlattenArray() {
|
||||
|
||||
}
|
||||
|
||||
toArray() {
|
||||
if (node.children && node.children.length > 0) {
|
||||
traversal(node.children, node);
|
||||
}
|
||||
});
|
||||
};
|
||||
traversal(this.collection);
|
||||
|
||||
return flattenTree;
|
||||
}
|
||||
}
|
||||
|
||||
6
server/src/data/ResourceFieldsKeys.js
Normal file
6
server/src/data/ResourceFieldsKeys.js
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
export default {
|
||||
"expense_account": 'expense_account_id',
|
||||
"payment_account": 'payment_account_id',
|
||||
"account_type": "account_type_id"
|
||||
}
|
||||
@@ -170,7 +170,7 @@ factory.define('resource_field', 'resource_fields', async () => {
|
||||
|
||||
return {
|
||||
label_name: faker.lorem.words(),
|
||||
slug: faker.lorem.slug(),
|
||||
key: faker.lorem.slug(),
|
||||
data_type: dataTypes[Math.floor(Math.random() * dataTypes.length)],
|
||||
help_text: faker.lorem.words(),
|
||||
default: faker.lorem.word(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
exports.up = (knex) => {
|
||||
return knex.schema.createTable('account_types', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
|
||||
@@ -3,12 +3,13 @@ exports.up = function (knex) {
|
||||
return knex.schema.createTable('resource_fields', (table) => {
|
||||
table.increments();
|
||||
table.string('label_name');
|
||||
table.string('slug');
|
||||
table.string('key');
|
||||
table.string('data_type');
|
||||
table.string('help_text');
|
||||
table.string('default');
|
||||
table.boolean('active');
|
||||
table.boolean('predefined');
|
||||
table.boolean('builtin').defaultTo(false);
|
||||
table.boolean('columnable');
|
||||
table.integer('index');
|
||||
table.json('options');
|
||||
|
||||
@@ -5,6 +5,7 @@ exports.up = function (knex) {
|
||||
table.string('name');
|
||||
table.boolean('predefined');
|
||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||
table.boolean('favourite');
|
||||
table.string('roles_logic_expression');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
|
||||
};
|
||||
64
server/src/database/seeds/account_types.js
Normal file
64
server/src/database/seeds/account_types.js
Normal file
@@ -0,0 +1,64 @@
|
||||
|
||||
exports.seed = (knex) => {
|
||||
// Deletes ALL existing entries
|
||||
return knex('account_types').del()
|
||||
.then(() => {
|
||||
// Inserts seed entries
|
||||
return knex('account_types').insert([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Fixed Asset',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Current Asset',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Long Term Liability',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Current Liability',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Equity',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Expense',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Income',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Accounts Receivable',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Accounts Payable',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
||||
100
server/src/database/seeds/accounts.js
Normal file
100
server/src/database/seeds/accounts.js
Normal file
@@ -0,0 +1,100 @@
|
||||
|
||||
exports.seed = (knex) => {
|
||||
// Deletes ALL existing entries
|
||||
return knex('accounts').del()
|
||||
.then(() => {
|
||||
// Inserts seed entries
|
||||
return knex('accounts').insert([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Petty Cash',
|
||||
account_type_id: 2,
|
||||
parent_account_id: null,
|
||||
code: '10000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bank',
|
||||
account_type_id: 2,
|
||||
parent_account_id: null,
|
||||
code: '20000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Other Income',
|
||||
account_type_id: 7,
|
||||
parent_account_id: null,
|
||||
code: '1000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Interest Income',
|
||||
account_type_id: 7,
|
||||
parent_account_id: null,
|
||||
code: '1000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Opening Balance',
|
||||
account_type_id: 5,
|
||||
parent_account_id: null,
|
||||
code: '1000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Depreciation Expense',
|
||||
account_type_id: 6,
|
||||
parent_account_id: null,
|
||||
code: '1000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Interest Expense',
|
||||
account_type_id: 6,
|
||||
parent_account_id: null,
|
||||
code: '1000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Payroll Expenses',
|
||||
account_type_id: 6,
|
||||
parent_account_id: null,
|
||||
code: '1000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Other Expenses',
|
||||
account_type_id: 6,
|
||||
parent_account_id: null,
|
||||
code: '1000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
}
|
||||
]);
|
||||
});
|
||||
};
|
||||
14
server/src/database/seeds/seed_accounts_fields.js
Normal file
14
server/src/database/seeds/seed_accounts_fields.js
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
exports.seed = function(knex) {
|
||||
// Deletes ALL existing entries
|
||||
return knex('resource_fields').del()
|
||||
.then(() => {
|
||||
// Inserts seed entries
|
||||
return knex('resource_fields').insert([
|
||||
{id: 1, label_name: 'Name', key: 'name', data_type: '', active: 1, predefined: 1},
|
||||
{id: 2, label_name: 'Code', key: 'code', data_type: '', active: 1, predefined: 1},
|
||||
{id: 3, label_name: 'Account Type', key: 'account_type_id', data_type: '', active: 1, predefined: 1},
|
||||
{id: 4, label_name: 'Description', key: 'description', data_type: '', active: 1, predefined: 1},
|
||||
]);
|
||||
});
|
||||
};
|
||||
13
server/src/database/seeds/seed_resources.js
Normal file
13
server/src/database/seeds/seed_resources.js
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
exports.seed = (knex) => {
|
||||
// Deletes ALL existing entries
|
||||
return knex('resources').del()
|
||||
.then(() => {
|
||||
// Inserts seed entries
|
||||
return knex('resources').insert([
|
||||
{ id: 1, name: 'accounts' },
|
||||
{ id: 2, name: 'items' },
|
||||
{ id: 3, name: 'expenses' },
|
||||
]);
|
||||
});
|
||||
};
|
||||
83
server/src/database/seeds/seed_resources_fields.js
Normal file
83
server/src/database/seeds/seed_resources_fields.js
Normal file
@@ -0,0 +1,83 @@
|
||||
|
||||
exports.seed = (knex) => {
|
||||
return knex('resource_fields').del()
|
||||
.then(() => {
|
||||
return knex('resource_fields').insert([
|
||||
// Accounts
|
||||
{
|
||||
id: 1,
|
||||
resource_id: 1,
|
||||
label_name: 'Account Name',
|
||||
data_type: 'textbox',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
resource_id: 1,
|
||||
label_name: 'Code',
|
||||
data_type: 'textbox',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
resource_id: 1,
|
||||
label_name: 'Type',
|
||||
data_type: 'options',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
resource_id: 1,
|
||||
label_name: 'Type',
|
||||
data_type: 'normal',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
resource_id: 1,
|
||||
label_name: 'Description',
|
||||
data_type: 'textarea',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
|
||||
// Expenses
|
||||
{
|
||||
id: 6,
|
||||
resource_id: 3,
|
||||
label_name: 'Date',
|
||||
data_type: 'date',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
resource_id: 3,
|
||||
label_name: 'Expense Account',
|
||||
data_type: 'options',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
resource_id: 3,
|
||||
label_name: 'Payment Account',
|
||||
data_type: 'options',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
resource_id: 3,
|
||||
label_name: 'Amount',
|
||||
data_type: 'number',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
||||
18
server/src/database/seeds/users.js
Normal file
18
server/src/database/seeds/users.js
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
exports.seed = (knex) => {
|
||||
return knex('users').del()
|
||||
.then(() => {
|
||||
return knex('users').insert([
|
||||
{
|
||||
first_name: 'Ahmed',
|
||||
last_name: 'Mohamed',
|
||||
email: 'admin@admin.com',
|
||||
phone_number: '0920000000',
|
||||
password: '$2b$10$LGSMrezP8IHBb/cNMlc1ZOKA59Fc9rY0IEk2u.iuF/y6yS2YlGP7i', // test
|
||||
active: 1,
|
||||
language: 'ar',
|
||||
created_at: new Date(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
||||
34
server/src/http/controllers/AccountTypes.js
Normal file
34
server/src/http/controllers/AccountTypes.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import express from 'express';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import AccountType from '@/models/AccountType';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
router.use(JWTAuth);
|
||||
|
||||
router.get('/',
|
||||
this.getAccountTypesList.validation,
|
||||
asyncMiddleware(this.getAccountTypesList.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve accounts types list.
|
||||
*/
|
||||
getAccountTypesList: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const accountTypes = await AccountType.query();
|
||||
|
||||
return res.status(200).send({
|
||||
account_types: accountTypes,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -52,7 +52,10 @@ export default {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
const form = {
|
||||
date: new Date(),
|
||||
...req.body,
|
||||
};
|
||||
const errorReasons = [];
|
||||
let totalCredit = 0;
|
||||
let totalDebit = 0;
|
||||
@@ -98,6 +101,7 @@ export default {
|
||||
const account = accounts.find((a) => a.id === entry.account_id);
|
||||
|
||||
const jouranlEntry = new JournalEntry({
|
||||
date: entry.date,
|
||||
debit: entry.debit,
|
||||
credit: entry.credit,
|
||||
account: account.id,
|
||||
|
||||
@@ -6,8 +6,14 @@ import AccountType from '@/models/AccountType';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import AccountBalance from '@/models/AccountBalance';
|
||||
import Resource from '@/models/Resource';
|
||||
import View from '@/models/View';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import NestedSet from '../../collection/NestedSet';
|
||||
import {
|
||||
mapViewRolesToConditionals,
|
||||
validateViewRoles,
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -162,12 +168,12 @@ export default {
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const account = await Account.where('id', id).fetch();
|
||||
const account = await Account.query().where('id', id).first();
|
||||
|
||||
if (!account) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
return res.status(200).send({ item: { ...account.attributes } });
|
||||
return res.status(200).send({ account: { ...account } });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -204,8 +210,10 @@ export default {
|
||||
*/
|
||||
getAccountsList: {
|
||||
validation: [
|
||||
query('display_type').optional().isIn(['tree', 'flat']),
|
||||
query('account_types').optional().isArray(),
|
||||
query('account_types.*').optional().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -216,19 +224,72 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
const form = {
|
||||
const filter = {
|
||||
account_types: [],
|
||||
...req.body,
|
||||
display_type: 'tree',
|
||||
...req.query,
|
||||
};
|
||||
const accounts = await Account.query()
|
||||
.modify('filterAccountTypes', form.account_types);
|
||||
const errorReasons = [];
|
||||
const viewConditionals = [];
|
||||
const accountsResource = await Resource.query().where('name', 'accounts').first();
|
||||
|
||||
const accountsNestedSet = new NestedSet(accounts, {
|
||||
parentId: 'parentAccountId',
|
||||
if (!accountsResource) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
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.withGraphFetched('roles.field');
|
||||
builder.withGraphFetched('columns');
|
||||
builder.first();
|
||||
});
|
||||
|
||||
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 (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const accounts = await Account.query().onBuild((builder) => {
|
||||
builder.modify('filterAccountTypes', filter.account_types);
|
||||
builder.withGraphFetched('type');
|
||||
|
||||
if (viewConditionals.length) {
|
||||
builder.modify('viewRolesBuilder', viewConditionals, view.rolesLogicExpression);
|
||||
}
|
||||
});
|
||||
|
||||
const nestedAccounts = new NestedSet(accounts, { parentId: 'parentAccountId' });
|
||||
const groupsAccounts = 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({
|
||||
// ...accountsNestedSet.toArray(),
|
||||
accounts: accountsList,
|
||||
...(view) ? {
|
||||
customViewId: view.id,
|
||||
} : {},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
login: {
|
||||
validation: [
|
||||
check('crediential').exists().isEmail(),
|
||||
check('password').exists().isLength({ min: 5 }),
|
||||
check('password').exists().isLength({ min: 4 }),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -64,12 +64,12 @@ export default {
|
||||
}
|
||||
if (!user.verifyPassword(password)) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INCORRECT_PASSWORD', code: 110 }],
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (!user.active) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_INACTIVE', code: 120 }],
|
||||
errors: [{ type: 'USER_INACTIVE', code: 110 }],
|
||||
});
|
||||
}
|
||||
// user.update({ last_login_at: new Date() });
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
}, JWT_SECRET_KEY, {
|
||||
expiresIn: '1d',
|
||||
});
|
||||
return res.status(200).send({ token });
|
||||
return res.status(200).send({ token, user });
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
46
server/src/http/controllers/Currencies.js
Normal file
46
server/src/http/controllers/Currencies.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
|
||||
export default {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/all',
|
||||
this.all.validation,
|
||||
asyncMiddleware(this.all.handler));
|
||||
|
||||
router.get('/registered',
|
||||
this.registered.validation,
|
||||
asyncMiddleware(this.registered.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
all: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
|
||||
return res.status(200).send({
|
||||
currencies: [
|
||||
{ currency_code: 'USD', currency_sign: '$' },
|
||||
{ currency_code: 'LYD', currency_sign: '' },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
registered: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
|
||||
return res.status(200).send({
|
||||
currencies: [
|
||||
{ currency_code: 'USD', currency_sign: '$' },
|
||||
{ currency_code: 'LYD', currency_sign: '' },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -17,6 +17,10 @@ import AccountTransaction from '@/models/AccountTransaction';
|
||||
import View from '@/models/View';
|
||||
import Resource from '../../models/Resource';
|
||||
import ResourceCustomFieldRepository from '@/services/CustomFields/ResourceCustomFieldRepository';
|
||||
import {
|
||||
validateViewRoles,
|
||||
mapViewRolesToConditionals,
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -50,9 +54,9 @@ export default {
|
||||
this.listExpenses.validation,
|
||||
asyncMiddleware(this.listExpenses.handler));
|
||||
|
||||
router.get('/:id',
|
||||
this.getExpense.validation,
|
||||
asyncMiddleware(this.getExpense.handler));
|
||||
// router.get('/:id',
|
||||
// this.getExpense.validation,
|
||||
// asyncMiddleware(this.getExpense.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
@@ -62,7 +66,7 @@ export default {
|
||||
*/
|
||||
newExpense: {
|
||||
validation: [
|
||||
check('date').optional().isISO8601(),
|
||||
check('date').optional(),
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('expense_account_id').exists().isNumeric().toInt(),
|
||||
check('description').optional(),
|
||||
@@ -90,7 +94,7 @@ export default {
|
||||
};
|
||||
// Convert the date to the general format.
|
||||
form.date = moment(form.date).format('YYYY-MM-DD');
|
||||
s
|
||||
|
||||
const errorReasons = [];
|
||||
const paymentAccount = await Account.query()
|
||||
.findById(form.payment_account_id).first();
|
||||
@@ -103,19 +107,19 @@ s
|
||||
if (!expenseAccount) {
|
||||
errorReasons.push({ type: 'EXPENSE.ACCOUNT.NOT.FOUND', code: 200 });
|
||||
}
|
||||
const customFields = new ResourceCustomFieldRepository(Expense);
|
||||
await customFields.load();
|
||||
// const customFields = new ResourceCustomFieldRepository(Expense);
|
||||
// await customFields.load();
|
||||
|
||||
if (customFields.validateExistCustomFields()) {
|
||||
errorReasons.push({ type: 'CUSTOM.FIELDS.SLUGS.NOT.EXISTS', code: 400 });
|
||||
}
|
||||
// if (customFields.validateExistCustomFields()) {
|
||||
// errorReasons.push({ type: 'CUSTOM.FIELDS.SLUGS.NOT.EXISTS', code: 400 });
|
||||
// }
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const expenseTransaction = await Expense.query().insertAndFetch({
|
||||
...omit(form, ['custom_fields']),
|
||||
});
|
||||
customFields.fillCustomFields(expenseTransaction.id, form.custom_fields);
|
||||
// customFields.fillCustomFields(expenseTransaction.id, form.custom_fields);
|
||||
|
||||
const journalEntries = new JournalPoster();
|
||||
const creditEntry = new JournalEntry({
|
||||
@@ -140,7 +144,7 @@ s
|
||||
journalEntries.debit(debitEntry);
|
||||
|
||||
await Promise.all([
|
||||
customFields.saveCustomFields(expenseTransaction.id),
|
||||
// customFields.saveCustomFields(expenseTransaction.id),
|
||||
journalEntries.saveEntries(),
|
||||
journalEntries.saveBalance(),
|
||||
]);
|
||||
@@ -331,38 +335,61 @@ s
|
||||
const expenseResource = await Resource.query().where('name', 'expenses').first();
|
||||
|
||||
if (!expenseResource) {
|
||||
errorReasons.push({ type: 'EXPENSE_NOT_FOUND', code: 300 });
|
||||
errorReasons.push({ type: 'EXPENSE_RESOURCE_NOT_FOUND', code: 300 });
|
||||
}
|
||||
const view = await View.query().runBefore((result, q) => {
|
||||
if (filter.customer_view_id) {
|
||||
q.where('id', filter.customer_view_id);
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const view = await View.query().onBuild((builder) => {
|
||||
if (filter.custom_view_id) {
|
||||
builder.where('id', filter.custom_view_id);
|
||||
} else {
|
||||
q.where('favorite', true);
|
||||
builder.where('favourite', true);
|
||||
}
|
||||
q.where('resource_id', expenseResource.id);
|
||||
q.withGraphFetched('viewRoles');
|
||||
q.withGraphFetched('columns');
|
||||
q.first();
|
||||
return result;
|
||||
});
|
||||
builder.where('resource_id', expenseResource.id);
|
||||
builder.withGraphFetched('viewRoles.field');
|
||||
builder.withGraphFetched('columns');
|
||||
|
||||
if (!view) {
|
||||
builder.first();
|
||||
});
|
||||
let viewConditionals = [];
|
||||
|
||||
if (view && view.viewRoles.length > 0) {
|
||||
viewConditionals = mapViewRolesToConditionals(view.viewRoles);
|
||||
|
||||
if (!validateViewRoles(viewConditionals, view.rolesLogicExpression)) {
|
||||
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 })
|
||||
}
|
||||
}
|
||||
if (!view && filter.custom_view_id) {
|
||||
errorReasons.push({ type: 'VIEW_NOT_FOUND', code: 100 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
const expenses = await Expense.query()
|
||||
.modify('filterByAmountRange', filter.range_from, filter.to_range)
|
||||
.modify('filterByDateRange', filter.date_from, filter.date_to)
|
||||
.modify('filterByExpenseAccount', filter.expense_account_id)
|
||||
.modify('filterByPaymentAccount', filter.payment_account_id)
|
||||
.modify('orderBy', filter.column_sort_order, filter.sort_order)
|
||||
.page(filter.page, filter.page_size);
|
||||
|
||||
|
||||
const expenses = await Expense.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('paymentAccount');
|
||||
builder.withGraphFetched('expenseAccount');
|
||||
builder.withGraphFetched('user');
|
||||
|
||||
if (viewConditionals.length) {
|
||||
builder.modify('viewRolesBuilder', viewConditionals, view.rolesLogicExpression);
|
||||
}
|
||||
builder.modify('filterByAmountRange', filter.range_from, filter.to_range);
|
||||
builder.modify('filterByDateRange', filter.date_from, filter.date_to);
|
||||
builder.modify('filterByExpenseAccount', filter.expense_account_id);
|
||||
builder.modify('filterByPaymentAccount', filter.payment_account_id);
|
||||
builder.modify('orderBy', filter.column_sort_order, filter.sort_order);
|
||||
}).page(filter.page - 1, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
columns: view.columns,
|
||||
viewRoles: view.viewRoles,
|
||||
...(view) ? {
|
||||
customViewId: view.id,
|
||||
viewColumns: view.columns,
|
||||
viewConditionals,
|
||||
} : {},
|
||||
expenses,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -226,7 +226,6 @@ export default {
|
||||
errors: [{ type: 'PREDEFINED_FIELD', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
await field.destroy();
|
||||
|
||||
return res.status(200).send({ id: field.get('id') });
|
||||
|
||||
@@ -289,6 +289,7 @@ export default {
|
||||
})),
|
||||
];
|
||||
return res.status(200).send({
|
||||
query: { ...filter },
|
||||
columns: { ...dateRangeSet },
|
||||
balance_sheet: {
|
||||
assets,
|
||||
|
||||
@@ -68,9 +68,7 @@ export default {
|
||||
}
|
||||
const options = await Option.query();
|
||||
|
||||
return res.status(200).sends({
|
||||
options: options.toArray(),
|
||||
});
|
||||
return res.status(200).sends({ options });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
94
server/src/http/controllers/Resources.js
Normal file
94
server/src/http/controllers/Resources.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
param,
|
||||
query,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import Resource from '@/models/Resource';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.use(jwtAuth);
|
||||
|
||||
router.get('/:resource_slug/columns',
|
||||
this.resourceColumns.validation,
|
||||
asyncMiddleware(this.resourceColumns.handler));
|
||||
|
||||
router.get('/:resource_slug/fields',
|
||||
this.resourceFields.validation,
|
||||
asyncMiddleware(this.resourceFields.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve resource columns of the given resource.
|
||||
*/
|
||||
resourceColumns: {
|
||||
validation: [
|
||||
param('resource_slug').trim().escape().exists(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { resource_slug: resourceSlug } = req.params;
|
||||
|
||||
const resource = await Resource.query()
|
||||
.where('name', resourceSlug)
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
|
||||
if (!resource) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'RESOURCE.SLUG.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
const resourceFields = resource.fields
|
||||
.filter((field) => field.columnable)
|
||||
.map((field) => ({
|
||||
id: field.id,
|
||||
label: field.labelName,
|
||||
key: field.key,
|
||||
}));
|
||||
|
||||
return res.status(200).send({
|
||||
resource_columns: resourceFields,
|
||||
resource_slug: resourceSlug,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve resource fields of the given resource.
|
||||
*/
|
||||
resourceFields: {
|
||||
validation: [
|
||||
param('resource_slug').trim().escape().exists(),
|
||||
query('predefined').optional().isBoolean().toBoolean(),
|
||||
query('builtin').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { resource_slug: resourceSlug } = req.params;
|
||||
|
||||
const resource = await Resource.query()
|
||||
.where('name', resourceSlug)
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
|
||||
if (!resource) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'RESOURCE.SLUG.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
resource_fields: resource.fields,
|
||||
resource_slug: resourceSlug,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,10 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import {
|
||||
check,
|
||||
query,
|
||||
param,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import User from '@/models/User';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
@@ -12,32 +17,32 @@ export default {
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
const permit = Authorization('users');
|
||||
// const permit = Authorization('users');
|
||||
|
||||
router.use(jwtAuth);
|
||||
|
||||
router.post('/',
|
||||
permit('create'),
|
||||
// permit('create'),
|
||||
this.newUser.validation,
|
||||
asyncMiddleware(this.newUser.handler));
|
||||
|
||||
router.post('/:id',
|
||||
permit('create', 'edit'),
|
||||
// permit('create', 'edit'),
|
||||
this.editUser.validation,
|
||||
asyncMiddleware(this.editUser.handler));
|
||||
|
||||
router.get('/',
|
||||
permit('view'),
|
||||
// permit('view'),
|
||||
this.listUsers.validation,
|
||||
asyncMiddleware(this.listUsers.handler));
|
||||
|
||||
router.get('/:id',
|
||||
permit('view'),
|
||||
// permit('view'),
|
||||
this.getUser.validation,
|
||||
asyncMiddleware(this.getUser.handler));
|
||||
|
||||
router.delete('/:id',
|
||||
permit('create', 'edit', 'delete'),
|
||||
// permit('create', 'edit', 'delete'),
|
||||
this.deleteUser.validation,
|
||||
asyncMiddleware(this.deleteUser.handler));
|
||||
|
||||
@@ -49,8 +54,8 @@ export default {
|
||||
*/
|
||||
newUser: {
|
||||
validation: [
|
||||
check('first_name').exists(),
|
||||
check('last_name').exists(),
|
||||
check('first_name').trim().escape().exists(),
|
||||
check('last_name').trim().escape().exists(),
|
||||
check('email').exists().isEmail(),
|
||||
check('phone_number').optional().isMobilePhone(),
|
||||
check('password').isLength({ min: 4 }).exists().custom((value, { req }) => {
|
||||
@@ -72,13 +77,12 @@ export default {
|
||||
}
|
||||
const { email, phone_number: phoneNumber } = req.body;
|
||||
|
||||
const foundUsers = await User.query((query) => {
|
||||
query.where('email', email);
|
||||
query.orWhere('phone_number', phoneNumber);
|
||||
}).fetchAll();
|
||||
const foundUsers = await User.query()
|
||||
.where('email', email)
|
||||
.orWhere('phone_number', phoneNumber);
|
||||
|
||||
const foundUserEmail = foundUsers.find((u) => u.attributes.email === email);
|
||||
const foundUserPhone = foundUsers.find((u) => u.attributes.phone_number === phoneNumber);
|
||||
const foundUserEmail = foundUsers.find((u) => u.email === email);
|
||||
const foundUserPhone = foundUsers.find((u) => u.phoneNumber === phoneNumber);
|
||||
|
||||
const errorReasons = [];
|
||||
|
||||
@@ -92,7 +96,7 @@ export default {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
const user = User.forge({
|
||||
const user = await User.query().insert({
|
||||
first_name: req.body.first_name,
|
||||
last_name: req.body.last_name,
|
||||
email: req.body.email,
|
||||
@@ -100,9 +104,7 @@ export default {
|
||||
active: req.body.status,
|
||||
});
|
||||
|
||||
await user.save();
|
||||
|
||||
return res.status(200).send({ id: user.get('id') });
|
||||
return res.status(200).send({ user });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -111,6 +113,7 @@ export default {
|
||||
*/
|
||||
editUser: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
check('first_name').exists(),
|
||||
check('last_name').exists(),
|
||||
check('email').exists().isEmail(),
|
||||
@@ -133,21 +136,22 @@ export default {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const user = await User.where('id', id).fetch();
|
||||
const user = await User.query().where('id', id).first();
|
||||
|
||||
if (!user) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
const { email, phone_number: phoneNumber } = req.body;
|
||||
|
||||
const foundUsers = await User.query((query) => {
|
||||
query.whereNot('id', id);
|
||||
query.where('email', email);
|
||||
query.orWhere('phone_number', phoneNumber);
|
||||
}).fetchAll();
|
||||
const foundUsers = await User.query()
|
||||
.whereNot('id', id)
|
||||
.andWhere((q) => {
|
||||
q.where('email', email);
|
||||
q.orWhere('phone_number', phoneNumber);
|
||||
});
|
||||
|
||||
const foundUserEmail = foundUsers.find((u) => u.attribues.email === email);
|
||||
const foundUserPhone = foundUsers.find((u) => u.attribues.phone_number === phoneNumber);
|
||||
const foundUserEmail = foundUsers.find((u) => u.email === email);
|
||||
const foundUserPhone = foundUsers.find((u) => u.phoneNumber === phoneNumber);
|
||||
|
||||
const errorReasons = [];
|
||||
|
||||
@@ -158,17 +162,16 @@ export default {
|
||||
errorReasons.push({ type: 'PHONE_NUMBER_ALREADY_EXIST', code: 120 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.badRequest(null, { errors: errorReasons });
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
await user.save({
|
||||
await User.query().where('id', id).update({
|
||||
first_name: req.body.first_name,
|
||||
last_name: req.body.last_name,
|
||||
email: req.body.email,
|
||||
phone_number: req.body.phone_number,
|
||||
status: req.body.status,
|
||||
active: req.body.status,
|
||||
});
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
@@ -180,30 +183,34 @@ export default {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const user = await User.where('id', id).fetch();
|
||||
const user = await User.query().where('id', id).first();
|
||||
|
||||
if (!user) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'USER_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
await User.query().where('id', id).delete();
|
||||
|
||||
await user.destroy();
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve user details of the given user id.
|
||||
*/
|
||||
getUser: {
|
||||
validation: [],
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const user = await User.where('id', id).fetch();
|
||||
const user = await User.query().where('id', id).first();
|
||||
|
||||
if (!user) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
|
||||
return res.status(200).send({ item: user.toJSON() });
|
||||
return res.status(200).send({ user });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -211,8 +218,11 @@ export default {
|
||||
* Retrieve the list of users.
|
||||
*/
|
||||
listUsers: {
|
||||
validation: [],
|
||||
handler(req, res) {
|
||||
validation: [
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const filter = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
@@ -224,28 +234,10 @@ export default {
|
||||
...req.query,
|
||||
};
|
||||
|
||||
const users = User.query((query) => {
|
||||
if (filter.first_name) {
|
||||
query.where('first_name', filter.first_name);
|
||||
}
|
||||
if (filter.last_name) {
|
||||
query.where('last_name', filter.last_name);
|
||||
}
|
||||
if (filter.email) {
|
||||
query.where('email', filter.email);
|
||||
}
|
||||
if (filter.phone_number) {
|
||||
query.where('phone_number', filter.phone_number);
|
||||
}
|
||||
}).fetchPage({
|
||||
page_size: filter.page_size,
|
||||
page: filter.page,
|
||||
});
|
||||
const users = await User.query()
|
||||
.page(filter.page - 1, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
items: users.toJSON(),
|
||||
pagination: users.pagination,
|
||||
});
|
||||
return res.status(200).send({ users });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { difference, pick } from 'lodash';
|
||||
import express from 'express';
|
||||
import { check, query, validationResult } from 'express-validator';
|
||||
import {
|
||||
check,
|
||||
query,
|
||||
param,
|
||||
oneOf,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import Resource from '@/models/Resource';
|
||||
import View from '@/models/View';
|
||||
import ViewRole from '@/models/ViewRole';
|
||||
import ViewColumn from '@/models/ViewColumn';
|
||||
import {
|
||||
validateViewLogicExpression,
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
|
||||
export default {
|
||||
resource: 'items',
|
||||
@@ -18,6 +28,10 @@ export default {
|
||||
|
||||
router.use(jwtAuth);
|
||||
|
||||
router.get('/',
|
||||
this.listViews.validation,
|
||||
asyncMiddleware(this.listViews.handler));
|
||||
|
||||
router.post('/',
|
||||
this.createView.validation,
|
||||
asyncMiddleware(this.createView.handler));
|
||||
@@ -33,10 +47,6 @@ export default {
|
||||
router.get('/:view_id',
|
||||
asyncMiddleware(this.getView.handler));
|
||||
|
||||
router.get('/resource/:resource_name',
|
||||
this.getResourceViews.validation,
|
||||
asyncMiddleware(this.getResourceViews.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
@@ -45,29 +55,53 @@ export default {
|
||||
*/
|
||||
listViews: {
|
||||
validation: [
|
||||
query('resource_name').optional().trim().escape(),
|
||||
oneOf([
|
||||
query('resource_name').exists().trim().escape(),
|
||||
], [
|
||||
query('resource_id').exists().isNumeric().toInt(),
|
||||
]),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { resource_id: resourceId } = req.params;
|
||||
const views = await View.where('resource_id', resourceId).fetchAll();
|
||||
const filter = { ...req.query };
|
||||
|
||||
return res.status(200).send({ views: views.toJSON() });
|
||||
const resource = await Resource.query().onBuild((builder) => {
|
||||
if (filter.resource_id) {
|
||||
builder.where('id', filter.resource_id);
|
||||
}
|
||||
if (filter.resource_name) {
|
||||
builder.where('name', filter.resource_name);
|
||||
}
|
||||
builder.first();
|
||||
});
|
||||
|
||||
const views = await View.query().where('resource_id', resource.id);
|
||||
|
||||
return res.status(200).send({ views });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve view details of the given view id.
|
||||
*/
|
||||
getView: {
|
||||
validation: [
|
||||
param('view_id').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { view_id: viewId } = req.params;
|
||||
const view = await View.where('id', viewId).fetch({
|
||||
withRelated: ['resource', 'columns', 'viewRoles'],
|
||||
});
|
||||
const view = await View.query()
|
||||
.where('id', viewId)
|
||||
.withGraphFetched('resource')
|
||||
.withGraphFetched('columns')
|
||||
.withGraphFetched('roles.field')
|
||||
.first();
|
||||
|
||||
if (!view) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
|
||||
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
return res.status(200).send({ ...view.toJSON() });
|
||||
return res.status(200).send({ view: view.toJSON() });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -75,7 +109,9 @@ export default {
|
||||
* Delete the given view of the resource.
|
||||
*/
|
||||
deleteView: {
|
||||
validation: [],
|
||||
validation: [
|
||||
param('view_id').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { view_id: viewId } = req.params;
|
||||
const view = await View.query().findById(viewId);
|
||||
@@ -91,12 +127,12 @@ export default {
|
||||
});
|
||||
}
|
||||
await Promise.all([
|
||||
view.$relatedQuery('viewRoles').delete(),
|
||||
view.$relatedQuery('roles').delete(),
|
||||
view.$relatedQuery('columns').delete(),
|
||||
]);
|
||||
await view.delete();
|
||||
await View.query().where('id', view.id).delete();
|
||||
|
||||
return res.status(200).send({ id: view.get('id') });
|
||||
return res.status(200).send({ id: view.id });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -106,15 +142,15 @@ export default {
|
||||
createView: {
|
||||
validation: [
|
||||
check('resource_name').exists().escape().trim(),
|
||||
check('label').exists().escape().trim(),
|
||||
check('columns').exists().isArray({ min: 1 }),
|
||||
check('roles').isArray(),
|
||||
check('roles.*.field').exists().escape().trim(),
|
||||
check('name').exists().escape().trim(),
|
||||
check('logic_expression').exists().trim().escape(),
|
||||
check('roles').isArray({ min: 1 }),
|
||||
check('roles.*.field_key').exists().escape().trim(),
|
||||
check('roles.*.comparator').exists(),
|
||||
check('roles.*.value').exists(),
|
||||
check('roles.*.index').exists().isNumeric().toInt(),
|
||||
check('columns').exists().isArray(),
|
||||
check('columns.*.field').exists().escape().trim(),
|
||||
check('columns').exists().isArray({ min: 1 }),
|
||||
check('columns.*.key').exists().escape().trim(),
|
||||
check('columns.*.index').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
@@ -134,10 +170,12 @@ export default {
|
||||
});
|
||||
}
|
||||
const errorReasons = [];
|
||||
const fieldsSlugs = form.roles.map((role) => role.field);
|
||||
const fieldsSlugs = form.roles.map((role) => role.field_key);
|
||||
|
||||
const resourceFields = await resource.$relatedQuery('fields');
|
||||
const resourceFieldsKeys = resourceFields.map((f) => f.slug);
|
||||
const resourceFieldsKeys = resourceFields.map((f) => f.key);
|
||||
const resourceFieldsKeysMap = new Map(resourceFields.map((field) => [field.key, field]));
|
||||
const columnsKeys = form.columns.map((c) => c.key);
|
||||
|
||||
// The difference between the stored resource fields and submit fields keys.
|
||||
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
|
||||
@@ -146,34 +184,49 @@ export default {
|
||||
errorReasons.push({ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields });
|
||||
}
|
||||
// The difference between the stored resource fields and the submit columns keys.
|
||||
const notFoundColumns = difference(form.columns, resourceFieldsKeys);
|
||||
const notFoundColumns = difference(columnsKeys, resourceFieldsKeys);
|
||||
|
||||
if (notFoundColumns.length > 0) {
|
||||
errorReasons.push({ type: 'COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns });
|
||||
}
|
||||
// Validates the view conditional logic expression.
|
||||
if (!validateViewLogicExpression(form.logic_expression, form.roles.map((r) => r.index))) {
|
||||
errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
// Save view details.
|
||||
const view = await View.query().insert({
|
||||
name: form.label,
|
||||
name: form.name,
|
||||
predefined: false,
|
||||
resource_id: resource.id,
|
||||
roles_logic_expression: form.logic_expression,
|
||||
});
|
||||
|
||||
// Save view roles async operations.
|
||||
const saveViewRolesOpers = [];
|
||||
|
||||
form.roles.forEach((role) => {
|
||||
const fieldModel = resourceFields.find((f) => f.slug === role.field);
|
||||
|
||||
const oper = ViewRole.query().insert({
|
||||
const fieldModel = resourceFieldsKeysMap.get(role.field_key);
|
||||
|
||||
const saveViewRoleOper = ViewRole.query().insert({
|
||||
...pick(role, ['comparator', 'value', 'index']),
|
||||
field_id: fieldModel.id,
|
||||
view_id: view.id,
|
||||
});
|
||||
saveViewRolesOpers.push(oper);
|
||||
saveViewRolesOpers.push(saveViewRoleOper);
|
||||
});
|
||||
|
||||
form.columns.forEach((column) => {
|
||||
const fieldModel = resourceFieldsKeysMap.get(column.key);
|
||||
|
||||
const saveViewColumnOper = ViewColumn.query().insert({
|
||||
field_id: fieldModel.id,
|
||||
view_id: view.id,
|
||||
index: column.index,
|
||||
});
|
||||
saveViewRolesOpers.push(saveViewColumnOper);
|
||||
});
|
||||
await Promise.all(saveViewRolesOpers);
|
||||
|
||||
@@ -183,6 +236,7 @@ export default {
|
||||
|
||||
editView: {
|
||||
validation: [
|
||||
param('view_id').exists().isNumeric().toInt(),
|
||||
check('label').exists().escape().trim(),
|
||||
check('columns').isArray({ min: 3 }),
|
||||
check('roles').isArray(),
|
||||
@@ -207,17 +261,7 @@ export default {
|
||||
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
getResourceViews: {
|
||||
validation: [
|
||||
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import Roles from '@/http/controllers/Roles';
|
||||
import Items from '@/http/controllers/Items';
|
||||
import ItemCategories from '@/http/controllers/ItemCategories';
|
||||
import Accounts from '@/http/controllers/Accounts';
|
||||
import AccountTypes from '@/http/controllers/AccountTypes';
|
||||
import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance';
|
||||
import Views from '@/http/controllers/Views';
|
||||
import CustomFields from '@/http/controllers/Fields';
|
||||
@@ -14,19 +15,23 @@ import Expenses from '@/http/controllers/Expenses';
|
||||
import Options from '@/http/controllers/Options';
|
||||
import Budget from '@/http/controllers/Budget';
|
||||
import BudgetReports from '@/http/controllers/BudgetReports';
|
||||
import Currencies from '@/http/controllers/Currencies';
|
||||
import Customers from '@/http/controllers/Customers';
|
||||
import Suppliers from '@/http/controllers/Suppliers';
|
||||
import Bills from '@/http/controllers/Bills';
|
||||
import CurrencyAdjustment from './controllers/CurrencyAdjustment';
|
||||
import Resources from './controllers/Resources';
|
||||
// import SalesReports from '@/http/controllers/SalesReports';
|
||||
// import PurchasesReports from '@/http/controllers/PurchasesReports';
|
||||
|
||||
export default (app) => {
|
||||
// app.use('/api/oauth2', OAuth2.router());
|
||||
app.use('/api/auth', Authentication.router());
|
||||
app.use('/api/currencies', Currencies.router());
|
||||
app.use('/api/users', Users.router());
|
||||
app.use('/api/roles', Roles.router());
|
||||
app.use('/api/accounts', Accounts.router());
|
||||
app.use('/api/account_types', AccountTypes.router());
|
||||
app.use('/api/accounting', Accounting.router());
|
||||
app.use('/api/accounts_opening_balances', AccountOpeningBalance.router());
|
||||
app.use('/api/views', Views.router());
|
||||
@@ -41,6 +46,7 @@ export default (app) => {
|
||||
// app.use('/api/suppliers', Suppliers.router());
|
||||
// app.use('/api/bills', Bills.router());
|
||||
app.use('/api/budget', Budget.router());
|
||||
app.use('/api/resources', Resources.router());
|
||||
// app.use('/api/currency_adjustment', CurrencyAdjustment.router());
|
||||
// app.use('/api/reports/sales', SalesReports.router());
|
||||
// app.use('/api/reports/purchases', PurchasesReports.router());
|
||||
|
||||
172
server/src/lib/LogicEvaluation/Lexer.js
Normal file
172
server/src/lib/LogicEvaluation/Lexer.js
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
const OperationType = {
|
||||
LOGIC: 'LOGIC',
|
||||
STRING: 'STRING',
|
||||
COMPARISON: 'COMPARISON',
|
||||
MATH: 'MATH',
|
||||
};
|
||||
|
||||
export class Lexer {
|
||||
// operation table
|
||||
static get optable() {
|
||||
return {
|
||||
'=': OperationType.LOGIC,
|
||||
'&': OperationType.LOGIC,
|
||||
'|': OperationType.LOGIC,
|
||||
'?': OperationType.LOGIC,
|
||||
':': OperationType.LOGIC,
|
||||
|
||||
'\'': OperationType.STRING,
|
||||
'"': OperationType.STRING,
|
||||
|
||||
'!': OperationType.COMPARISON,
|
||||
'>': OperationType.COMPARISON,
|
||||
'<': OperationType.COMPARISON,
|
||||
|
||||
'(': OperationType.MATH,
|
||||
')': OperationType.MATH,
|
||||
'+': OperationType.MATH,
|
||||
'-': OperationType.MATH,
|
||||
'*': OperationType.MATH,
|
||||
'/': OperationType.MATH,
|
||||
'%': OperationType.MATH,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {*} expression -
|
||||
*/
|
||||
constructor(expression) {
|
||||
this.currentIndex = 0;
|
||||
this.input = expression;
|
||||
this.tokenList = [];
|
||||
}
|
||||
|
||||
getTokens() {
|
||||
let tok;
|
||||
do {
|
||||
// read current token, so step should be -1
|
||||
tok = this.pickNext(-1);
|
||||
const pos = this.currentIndex;
|
||||
switch (Lexer.optable[tok]) {
|
||||
case OperationType.LOGIC:
|
||||
// == && || ===
|
||||
this.readLogicOpt(tok);
|
||||
break;
|
||||
|
||||
case OperationType.STRING:
|
||||
this.readString(tok);
|
||||
break;
|
||||
|
||||
case OperationType.COMPARISON:
|
||||
this.readCompare(tok);
|
||||
break;
|
||||
|
||||
case OperationType.MATH:
|
||||
this.receiveToken();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.readValue(tok);
|
||||
}
|
||||
|
||||
// if the pos not changed, this loop will go into a infinite loop, every step of while loop,
|
||||
// we must move the pos forward
|
||||
// so here we should throw error, for example `1 & 2`
|
||||
if (pos === this.currentIndex && tok !== undefined) {
|
||||
const err = new Error(`unkonw token ${tok} from input string ${this.input}`);
|
||||
err.name = 'UnknowToken';
|
||||
throw err;
|
||||
}
|
||||
} while (tok !== undefined)
|
||||
|
||||
return this.tokenList;
|
||||
}
|
||||
|
||||
/**
|
||||
* read next token, the index param can set next step, default go foward 1 step
|
||||
*
|
||||
* @param index next postion
|
||||
*/
|
||||
pickNext(index = 0) {
|
||||
return this.input[index + this.currentIndex + 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Store token into result tokenList, and move the pos index
|
||||
*
|
||||
* @param index
|
||||
*/
|
||||
receiveToken(index = 1) {
|
||||
const tok = this.input.slice(this.currentIndex, this.currentIndex + index).trim();
|
||||
// skip empty string
|
||||
if (tok) {
|
||||
this.tokenList.push(tok);
|
||||
}
|
||||
|
||||
this.currentIndex += index;
|
||||
}
|
||||
|
||||
// ' or "
|
||||
readString(tok) {
|
||||
let next;
|
||||
let index = 0;
|
||||
do {
|
||||
next = this.pickNext(index);
|
||||
index += 1;
|
||||
} while (next !== tok && next !== undefined);
|
||||
this.receiveToken(index + 1);
|
||||
}
|
||||
|
||||
// > or < or >= or <= or !==
|
||||
// tok in (>, <, !)
|
||||
readCompare(tok) {
|
||||
if (this.pickNext() !== '=') {
|
||||
this.receiveToken(1);
|
||||
return;
|
||||
}
|
||||
// !==
|
||||
if (tok === '!' && this.pickNext(1) === '=') {
|
||||
this.receiveToken(3);
|
||||
return;
|
||||
}
|
||||
this.receiveToken(2);
|
||||
}
|
||||
|
||||
// === or ==
|
||||
// && ||
|
||||
readLogicOpt(tok) {
|
||||
if (this.pickNext() === tok) {
|
||||
// ===
|
||||
if (tok === '=' && this.pickNext(1) === tok) {
|
||||
return this.receiveToken(3);
|
||||
}
|
||||
// == && ||
|
||||
return this.receiveToken(2);
|
||||
}
|
||||
// handle as &&
|
||||
// a ? b : c is equal to a && b || c
|
||||
if (tok === '?' || tok === ':') {
|
||||
return this.receiveToken(1);
|
||||
}
|
||||
}
|
||||
|
||||
readValue(tok) {
|
||||
if (!tok) {
|
||||
return;
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
while (!Lexer.optable[tok] && tok !== undefined) {
|
||||
tok = this.pickNext(index);
|
||||
index += 1;
|
||||
}
|
||||
this.receiveToken(index);
|
||||
}
|
||||
}
|
||||
|
||||
export default function token(expression) {
|
||||
const lexer = new Lexer(expression);
|
||||
return lexer.getTokens();
|
||||
}
|
||||
159
server/src/lib/LogicEvaluation/Parser.js
Normal file
159
server/src/lib/LogicEvaluation/Parser.js
Normal file
@@ -0,0 +1,159 @@
|
||||
export const OPERATION = {
|
||||
'!': 5,
|
||||
'*': 4,
|
||||
'/': 4,
|
||||
'%': 4,
|
||||
'+': 3,
|
||||
'-': 3,
|
||||
'>': 2,
|
||||
'<': 2,
|
||||
'>=': 2,
|
||||
'<=': 2,
|
||||
'===': 2,
|
||||
'!==': 2,
|
||||
'==': 2,
|
||||
'!=': 2,
|
||||
'&&': 1,
|
||||
'||': 1,
|
||||
'?': 1,
|
||||
':': 1,
|
||||
};
|
||||
|
||||
// export interface Node {
|
||||
// left: Node | string | null;
|
||||
// right: Node | string | null;
|
||||
// operation: string;
|
||||
// grouped?: boolean;
|
||||
// };
|
||||
|
||||
export default class Parser {
|
||||
|
||||
constructor(token) {
|
||||
this.index = -1;
|
||||
this.blockLevel = 0;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {Node | string} =-
|
||||
*/
|
||||
parse() {
|
||||
let tok;
|
||||
let root = {
|
||||
left: null,
|
||||
right: null,
|
||||
operation: null,
|
||||
};
|
||||
|
||||
do {
|
||||
tok = this.parseStatement();
|
||||
|
||||
if (tok === null || tok === undefined) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (root.left === null) {
|
||||
root.left = tok;
|
||||
root.operation = this.nextToken();
|
||||
|
||||
if (!root.operation) {
|
||||
return tok;
|
||||
}
|
||||
|
||||
root.right = this.parseStatement();
|
||||
} else {
|
||||
if (typeof tok !== 'string') {
|
||||
throw new Error('operation must be string, but get ' + JSON.stringify(tok));
|
||||
}
|
||||
root = this.addNode(tok, this.parseStatement(), root);
|
||||
}
|
||||
} while (tok);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
nextToken() {
|
||||
this.index += 1;
|
||||
return this.token[this.index];
|
||||
}
|
||||
|
||||
prevToken() {
|
||||
return this.token[this.index - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} operation
|
||||
* @param {Node|String|null} right
|
||||
* @param {Node} root
|
||||
*/
|
||||
addNode(operation, right, root) {
|
||||
let pre = root;
|
||||
|
||||
if (this.compare(pre.operation, operation) < 0 && !pre.grouped) {
|
||||
|
||||
while (pre.right !== null &&
|
||||
typeof pre.right !== 'string' &&
|
||||
this.compare(pre.right.operation, operation) < 0 && !pre.right.grouped) {
|
||||
pre = pre.right;
|
||||
}
|
||||
|
||||
pre.right = {
|
||||
operation,
|
||||
left: pre.right,
|
||||
right,
|
||||
};
|
||||
return root;
|
||||
}
|
||||
return {
|
||||
left: pre,
|
||||
right,
|
||||
operation,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} a
|
||||
* @param {String} b
|
||||
*/
|
||||
compare(a, b) {
|
||||
if (!OPERATION.hasOwnProperty(a) || !OPERATION.hasOwnProperty(b)) {
|
||||
throw new Error(`unknow operation ${a} or ${b}`);
|
||||
}
|
||||
return OPERATION[a] - OPERATION[b];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string | Node | null
|
||||
*/
|
||||
parseStatement() {
|
||||
const token = this.nextToken();
|
||||
if (token === '(') {
|
||||
this.blockLevel += 1;
|
||||
const node = this.parse();
|
||||
this.blockLevel -= 1;
|
||||
|
||||
if (typeof node !== 'string') {
|
||||
node.grouped = true;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
if (token === ')') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (token === '!') {
|
||||
return { left: null, operation: token, right: this.parseStatement() }
|
||||
}
|
||||
|
||||
// 3 > -12 or -12 + 10
|
||||
if (token === '-' && (OPERATION[this.prevToken()] > 0 || this.prevToken() === undefined)) {
|
||||
return { left: '0', operation: token, right: this.parseStatement(), grouped: true };
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
61
server/src/lib/LogicEvaluation/QueryParser.js
Normal file
61
server/src/lib/LogicEvaluation/QueryParser.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { OPERATION } from './Parser';
|
||||
|
||||
export default class QueryParser {
|
||||
|
||||
constructor(tree, queries) {
|
||||
this.tree = tree;
|
||||
this.queries = queries;
|
||||
this.query = null;
|
||||
}
|
||||
|
||||
setQuery(query) {
|
||||
this.query = query.clone();
|
||||
}
|
||||
|
||||
parse() {
|
||||
return this.parseNode(this.tree);
|
||||
}
|
||||
|
||||
parseNode(node) {
|
||||
if (typeof node === 'string') {
|
||||
const nodeQuery = this.getQuery(node);
|
||||
return (query) => { nodeQuery(query); };
|
||||
}
|
||||
if (OPERATION[node.operation] === undefined) {
|
||||
throw new Error(`unknow expression ${node.operation}`);
|
||||
}
|
||||
const leftQuery = this.getQuery(node.left);
|
||||
const rightQuery = this.getQuery(node.right);
|
||||
|
||||
switch (node.operation) {
|
||||
case '&&':
|
||||
case 'AND':
|
||||
default:
|
||||
return (nodeQuery) => nodeQuery.where((query) => {
|
||||
query.where((q) => { leftQuery(q); });
|
||||
query.andWhere((q) => { rightQuery(q); });
|
||||
});
|
||||
case '||':
|
||||
case 'OR':
|
||||
return (nodeQuery) => nodeQuery.where((query) => {
|
||||
query.where((q) => { leftQuery(q); });
|
||||
query.orWhere((q) => { rightQuery(q); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getQuery(node) {
|
||||
if (typeof node !== 'string' && node !== null) {
|
||||
return this.parseNode(node);
|
||||
}
|
||||
const value = parseFloat(node);
|
||||
|
||||
if (!isNaN(value)) {
|
||||
if (typeof this.queries[node] === 'undefined') {
|
||||
throw new Error(`unknow query under index ${node}`);
|
||||
}
|
||||
return this.queries[node];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
0
server/src/lib/LogicEvaluation/index.js
Normal file
0
server/src/lib/LogicEvaluation/index.js
Normal file
89
server/src/lib/ViewRolesBuilder/index.js
Normal file
89
server/src/lib/ViewRolesBuilder/index.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { difference } from 'lodash';
|
||||
import { Lexer } from '@/lib/LogicEvaluation/Lexer';
|
||||
import Parser from '@/lib/LogicEvaluation/Parser';
|
||||
import QueryParser from '@/lib/LogicEvaluation/QueryParser';
|
||||
import resourceFieldsKeys from '@/data/ResourceFieldsKeys';
|
||||
|
||||
// const role = {
|
||||
// compatotor: '',
|
||||
// value: '',
|
||||
// columnKey: '',
|
||||
// columnSlug: '',
|
||||
// index: 1,
|
||||
// }
|
||||
|
||||
export function buildRoleQuery(role) {
|
||||
const columnName = resourceFieldsKeys[role.columnKey];
|
||||
|
||||
switch (role.comparator) {
|
||||
case 'equals':
|
||||
default:
|
||||
return (builder) => {
|
||||
builder.where(columnName, role.value);
|
||||
};
|
||||
case 'not_equal':
|
||||
case 'not_equals':
|
||||
return (builder) => {
|
||||
builder.whereNot(columnName, role.value);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds database query from stored view roles.
|
||||
*
|
||||
* @param {Array} roles -
|
||||
* @return {Function}
|
||||
*/
|
||||
export function viewRolesBuilder(roles, logicExpression = '') {
|
||||
const rolesIndexSet = {};
|
||||
|
||||
roles.forEach((role) => {
|
||||
rolesIndexSet[role.index] = buildRoleQuery(role);
|
||||
});
|
||||
// Lexer for logic expression.
|
||||
const lexer = new Lexer(logicExpression);
|
||||
const tokens = lexer.getTokens();
|
||||
|
||||
// Parse the logic expression.
|
||||
const parser = new Parser(tokens);
|
||||
const parsedTree = parser.parse();
|
||||
|
||||
const queryParser = new QueryParser(parsedTree, rolesIndexSet);
|
||||
return queryParser.parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the view logic expression.
|
||||
* @param {String} logicExpression
|
||||
* @param {Array} indexes
|
||||
*/
|
||||
export function validateViewLogicExpression(logicExpression, indexes) {
|
||||
const logicExpIndexes = logicExpression.match(/\d+/g) || [];
|
||||
return !difference(logicExpIndexes.map(Number), indexes).length;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Array} roles -
|
||||
* @param {String} logicExpression -
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export function validateViewRoles(roles, logicExpression) {
|
||||
return validateViewLogicExpression(logicExpression, roles.map((r) => r.index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapes the view roles to view conditionals.
|
||||
* @param {Array} viewRoles -
|
||||
* @return {Array}
|
||||
*/
|
||||
export function mapViewRolesToConditionals(viewRoles) {
|
||||
return viewRoles.map((viewRole) => ({
|
||||
comparator: viewRole.comparator,
|
||||
value: viewRole.value,
|
||||
columnKey: viewRole.field.columnKey,
|
||||
slug: viewRole.field.slug,
|
||||
index: viewRole.index,
|
||||
}));
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Model } from 'objection';
|
||||
import { flatten } from 'lodash';
|
||||
import BaseModel from '@/models/Model';
|
||||
import {viewRolesBuilder} from '@/lib/ViewRolesBuilder';
|
||||
|
||||
export default class Account extends BaseModel {
|
||||
/**
|
||||
@@ -21,6 +22,9 @@ export default class Account extends BaseModel {
|
||||
query.whereIn('accoun_type_id', typesIds);
|
||||
}
|
||||
},
|
||||
viewRolesBuilder(query, conditionals, expression) {
|
||||
viewRolesBuilder(conditionals, expression)(query);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Model } from 'objection';
|
||||
import BaseModel from '@/models/Model';
|
||||
|
||||
import {viewRolesBuilder} from '@/lib/ViewRolesBuilder';
|
||||
export default class Expense extends BaseModel {
|
||||
/**
|
||||
* Table name
|
||||
@@ -44,6 +44,14 @@ export default class Expense extends BaseModel {
|
||||
query.where('payment_account_id', accountId);
|
||||
}
|
||||
},
|
||||
|
||||
viewRolesBuilder(query, conditionals, expression) {
|
||||
viewRolesBuilder(conditionals, expression)(query);
|
||||
},
|
||||
|
||||
orderBy(query) {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Model } from 'objection';
|
||||
import {transform, snakeCase} from 'lodash';
|
||||
import {mapKeysDeep} from '@/utils';
|
||||
|
||||
export default class ModelBase extends Model {
|
||||
static get collection() {
|
||||
@@ -13,4 +15,13 @@ export default class ModelBase extends Model {
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
$formatJson(json, opt) {
|
||||
const transformed = mapKeysDeep(json, (value, key) => {
|
||||
return snakeCase(key);
|
||||
});
|
||||
const parsedJson = super.$formatJson(transformed, opt);
|
||||
|
||||
return parsedJson;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class Role extends BaseModel {
|
||||
const Permission = require('@/models/Permission');
|
||||
const Resource = require('@/models/Resource');
|
||||
const User = require('@/models/User');
|
||||
const ResourceField = require('@/models/ResourceField');
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -58,6 +59,18 @@ export default class Role extends BaseModel {
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Role may has resource field.
|
||||
*/
|
||||
field: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: ResourceField.default,
|
||||
join: {
|
||||
from: 'roles.fieldId',
|
||||
to: 'resource_fields.id',
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Role may has many associated users.
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,10 @@ import BaseModel from '@/models/Model';
|
||||
export default class User extends BaseModel {
|
||||
// ...PermissionsService
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['fullName'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -43,4 +47,8 @@ export default class User extends BaseModel {
|
||||
verifyPassword(password) {
|
||||
return bcrypt.compareSync(password, this.password);
|
||||
}
|
||||
|
||||
fullName() {
|
||||
return `${this.firstName} ${this.lastName}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,19 +39,19 @@ export default class View extends BaseModel {
|
||||
modelClass: ViewColumn.default,
|
||||
join: {
|
||||
from: 'views.id',
|
||||
to: 'view_has_columns.view_id',
|
||||
to: 'view_has_columns.viewId',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* View model may has many view roles.
|
||||
*/
|
||||
viewRoles: {
|
||||
roles: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: ViewRole.default,
|
||||
join: {
|
||||
from: 'views.id',
|
||||
to: 'view_roles.view_id',
|
||||
to: 'view_roles.viewId',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { Model } from 'objection';
|
||||
import path from 'path';
|
||||
import BaseModel from '@/models/Model';
|
||||
|
||||
export default class ViewRole extends BaseModel {
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['comparators'];
|
||||
}
|
||||
|
||||
static get comparators() {
|
||||
return [
|
||||
'equals', 'not_equal', 'contains', 'not_contain',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
@@ -21,18 +34,33 @@ export default class ViewRole extends BaseModel {
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const ResourceField = require('@/models/ResourceField');
|
||||
const View = require('@/models/View');
|
||||
|
||||
return {
|
||||
/**
|
||||
* View role model may belongs to view model.
|
||||
*/
|
||||
view: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelBase: path.join(__dirname, 'View'),
|
||||
modelClass: View.default,
|
||||
join: {
|
||||
from: 'view_roles.view_id',
|
||||
from: 'view_roles.viewId',
|
||||
to: 'views.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* View role model may belongs to resource field model.
|
||||
*/
|
||||
field: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: ResourceField.default,
|
||||
join: {
|
||||
from: 'view_roles.fieldId',
|
||||
to: 'resource_fields.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
const { map, isArray, isPlainObject, mapKeys, mapValues } = require('lodash')
|
||||
|
||||
|
||||
const hashPassword = (password) => new Promise((resolve) => {
|
||||
bcrypt.genSalt(10, (error, salt) => {
|
||||
@@ -43,9 +46,31 @@ const dateRangeFormat = (rangeType) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const mapKeysDeep = (obj, cb) => {
|
||||
if (_.isArray(obj)) {
|
||||
return obj.map(innerObj => mapKeysDeep(innerObj, cb));
|
||||
}
|
||||
else if (_.isObject(obj)) {
|
||||
return _.mapValues(
|
||||
_.mapKeys(obj, cb),
|
||||
val => mapKeysDeep(val, cb),
|
||||
)
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
const mapValuesDeep = (v, callback) => (
|
||||
_.isObject(v)
|
||||
? _.mapValues(v, v => mapValuesDeep(v, callback))
|
||||
: callback(v));
|
||||
|
||||
export {
|
||||
hashPassword,
|
||||
origin,
|
||||
dateRangeCollection,
|
||||
dateRangeFormat,
|
||||
mapValuesDeep,
|
||||
mapKeysDeep,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user