This commit is contained in:
Ahmed Bouhuolia
2020-03-16 00:06:15 +02:00
parent 56701951b7
commit 73711384f6
7925 changed files with 18478 additions and 959 deletions

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
export default {
"expense_account": 'expense_account_id',
"payment_account": 'payment_account_id',
"account_type": "account_type_id"
}

View File

@@ -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(),

View File

@@ -1,5 +1,5 @@
exports.up = function (knex) {
exports.up = (knex) => {
return knex.schema.createTable('account_types', (table) => {
table.increments();
table.string('name');

View File

@@ -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');

View File

@@ -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');
});
};

View File

@@ -1,8 +0,0 @@
exports.up = function(knex) {
};
exports.down = function(knex) {
};

View 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,
},
]);
});
};

View 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,
}
]);
});
};

View 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},
]);
});
};

View 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' },
]);
});
};

View 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,
},
]);
});
};

View 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(),
},
]);
});
};

View 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,
});
},
},
};

View File

@@ -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,

View File

@@ -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,
} : {},
});
},
},

View File

@@ -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 });
},
},

View 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: '' },
],
});
},
},
};

View File

@@ -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,
});
},
},

View File

@@ -226,7 +226,6 @@ export default {
errors: [{ type: 'PREDEFINED_FIELD', code: 100 }],
});
}
await field.destroy();
return res.status(200).send({ id: field.get('id') });

View File

@@ -289,6 +289,7 @@ export default {
})),
];
return res.status(200).send({
query: { ...filter },
columns: { ...dateRangeSet },
balance_sheet: {
assets,

View File

@@ -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 });
},
},
};

View 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,
});
},
},
};

View File

@@ -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 });
},
},
};

View File

@@ -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) {
},
},
};

View File

@@ -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());

View 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();
}

View 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;
}
}

View 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;
}
}

View File

View 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,
}));
}

View File

@@ -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);
},
};
}

View File

@@ -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) {
}
};
}

View File

@@ -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;
}
}

View File

@@ -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.
*/

View File

@@ -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}`;
}
}

View File

@@ -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',
},
},
};

View File

@@ -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',
},
},
};
}
}

View File

@@ -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,
};