Custom fields feature.

This commit is contained in:
Ahmed Bouhuolia
2019-09-13 20:24:09 +02:00
parent cba17739d6
commit ed4d37c8fb
64 changed files with 2307 additions and 121 deletions

View File

@@ -6,7 +6,9 @@ import Account from '@/models/Account';
// import AccountBalance from '@/models/AccountBalance';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
@@ -17,6 +19,11 @@ export default {
return router;
},
/**
* Opening balance to the given account.
* @param {Request} req -
* @param {Response} res -
*/
openingBalnace: {
validation: [
check('accounts').isArray({ min: 1 }),

View File

@@ -1,8 +1,8 @@
import express from 'express';
import { check, validationResult } from 'express-validator';
import { check, validationResult, param } from 'express-validator';
import asyncMiddleware from '../middleware/asyncMiddleware';
import Account from '@/models/Account';
import AccountBalance from '@/models/AccountBalance';
// import AccountBalance from '@/models/AccountBalance';
import AccountType from '@/models/AccountType';
// import JWTAuth from '@/http/middleware/jwtAuth';
@@ -22,13 +22,12 @@ export default {
this.editAccount.validation,
asyncMiddleware(this.editAccount.handler));
// router.get('/:id',
// this.getAccount.validation,
// asyncMiddleware(this.getAccount.handler));
router.get('/:id',
asyncMiddleware(this.getAccount.handler));
// router.delete('/:id',
// this.deleteAccount.validation,
// asyncMiddleware(this.deleteAccount.handler));
router.delete('/:id',
this.deleteAccount.validation,
asyncMiddleware(this.deleteAccount.handler));
return router;
},
@@ -87,6 +86,7 @@ export default {
*/
editAccount: {
validation: [
param('id').toInt(),
check('name').isLength({ min: 3 }).trim().escape(),
check('code').isLength({ max: 10 }).trim().escape(),
check('account_type_id').isNumeric().toInt(),
@@ -142,7 +142,9 @@ export default {
* Get details of the given account.
*/
getAccount: {
valiation: [],
valiation: [
param('id').toInt(),
],
async handler(req, res) {
const { id } = req.params;
const account = await Account.where('id', id).fetch();
@@ -159,7 +161,9 @@ export default {
* Delete the given account.
*/
deleteAccount: {
validation: [],
validation: [
param('id').toInt(),
],
async handler(req, res) {
const { id } = req.params;
const account = await Account.where('id', id).fetch();
@@ -168,7 +172,6 @@ export default {
return res.boom.notFound();
}
await account.destroy();
await AccountBalance.where('account_id', id).destroy({ require: false });
return res.status(200).send({ id: account.previous('id') });
},

View File

@@ -92,7 +92,7 @@ export default {
});
}
const { email } = req.body;
const user = User.where('email', email).fetch();
const user = await User.where('email', email).fetch();
if (!user) {
return res.status(422).send();

View File

@@ -0,0 +1,246 @@
import express from 'express';
import { check, param, validationResult } from 'express-validator';
import ResourceField from '@/models/ResourceField';
import Resource from '@/models/Resource';
import asyncMiddleware from '../middleware/asyncMiddleware';
/**
* Types of the custom fields.
*/
const TYPES = ['text', 'email', 'number', 'url', 'percentage', 'checkbox', 'radio', 'textarea'];
export default {
/**
* Router constructor method.
*/
router() {
const router = express.Router();
router.post('/resource/:resource_id',
this.addNewField.validation,
asyncMiddleware(this.addNewField.handler));
router.post('/:field_id',
this.editField.validation,
asyncMiddleware(this.editField.handler));
router.post('/status/:field_id',
this.changeStatus.validation,
asyncMiddleware(this.changeStatus.handler));
router.get('/:field_id',
asyncMiddleware(this.getField.handler));
router.delete('/:field_id',
asyncMiddleware(this.deleteField.handler));
return router;
},
/**
* Adds a new field control to the given resource.
* @param {Request} req -
* @param {Response} res -
*/
addNewField: {
validation: [
param('resource_id').toInt(),
check('label').exists().escape().trim(),
check('data_type').exists().isIn(TYPES),
check('help_text').optional(),
check('default').optional(),
check('options').optional().isArray(),
],
async handler(req, res) {
const { resource_id: resourceId } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'VALIDATION_ERROR', ...validationErrors,
});
}
const resource = await Resource.where('id', resourceId).fetch();
if (!resource) {
return res.boom.notFound(null, {
errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }],
});
}
const { label, data_type: dataType, help_text: helpText } = req.body;
const { default: defaultValue, options } = req.body;
const choices = options.map((option, index) => ({ key: index + 1, value: option }));
const field = ResourceField.forge({
data_type: dataType,
label_name: label,
help_text: helpText,
default: defaultValue,
resource_id: resource.id,
options: choices,
});
await field.save();
return res.status(200).send();
},
},
/**
* Edit details of the given field.
*/
editField: {
validation: [
param('field_id').toInt(),
check('label').exists().escape().trim(),
check('data_type').exists(),
check('help_text').optional(),
check('default').optional(),
check('options').optional().isArray(),
],
async handler(req, res) {
const { field_id: fieldId } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'VALIDATION_ERROR', ...validationErrors,
});
}
const field = await ResourceField.where('id', fieldId).fetch();
if (!field) {
return res.boom.notFound(null, {
errors: [{ type: 'FIELD_NOT_FOUND', code: 100 }],
});
}
// Sets the default value of optional fields.
const form = { options: [], ...req.body };
const { label, data_type: dataType, help_text: helpText } = form;
const { default: defaultValue, options } = form;
const storedFieldOptions = field.attributes.options || [];
let lastChoiceIndex = 0;
storedFieldOptions.forEach((option) => {
const key = parseInt(option.key, 10);
if (key > lastChoiceIndex) {
lastChoiceIndex = key;
}
});
const savedOptionKeys = options.filter((op) => typeof op === 'object');
const notSavedOptionsKeys = options.filter((op) => typeof op !== 'object');
const choices = [
...savedOptionKeys,
...notSavedOptionsKeys.map((option) => {
lastChoiceIndex += 1;
return { key: lastChoiceIndex, value: option };
}),
];
await field.save({
data_type: dataType,
label_name: label,
help_text: helpText,
default: defaultValue,
options: choices,
});
return res.status(200).send({ id: field.get('id') });
},
},
/**
* Retrieve the fields list of the given resource.
* @param {Request} req -
* @param {Response} res -
*/
fieldsList: {
validation: [
param('resource_id').toInt(),
],
async handler(req, res) {
const { resource_id: resourceId } = req.params;
const fields = await ResourceField.where('resource_id', resourceId).fetchAll();
return res.status(200).send({ fields: fields.toJSON() });
},
},
/**
* Change status of the given field.
*/
changeStatus: {
validation: [
param('field_id').toInt(),
check('active').isBoolean().toBoolean(),
],
async handler(req, res) {
const { field_id: fieldId } = req.params;
const field = await ResourceField.where('id', fieldId).fetch();
if (!field) {
return res.boom.notFound(null, {
errors: [{ type: 'NOT_FOUND_FIELD', code: 100 }],
});
}
const { active } = req.body;
await field.save({ active });
return res.status(200).send({ id: field.get('id') });
},
},
/**
* Retrieve details of the given field.
*/
getField: {
validation: [
param('field_id').toInt(),
],
async handler(req, res) {
const { field_id: id } = req.params;
const field = await ResourceField.where('id', id).fetch();
if (!field) {
return res.boom.notFound();
}
return res.status(200).send({
field: field.toJSON(),
});
},
},
/**
* Delete the given field.
*/
deleteField: {
validation: [
param('field_id').toInt(),
],
async handler(req, res) {
const { field_id: id } = req.params;
const field = await ResourceField.where('id', id).fetch();
if (!field) {
return res.boom.notFound();
}
if (field.attributes.predefined) {
return res.boom.badRequest(null, {
errors: [{ type: 'PREDEFINED_FIELD', code: 100 }],
});
}
await field.destroy();
return res.status(200).send({ id: field.get('id') });
},
},
};

View File

@@ -1,5 +1,5 @@
import express from 'express';
import { check, validationResult } from 'express-validator';
import { check, param, validationResult } from 'express-validator';
import asyncMiddleware from '../middleware/asyncMiddleware';
import ItemCategory from '@/models/ItemCategory';
// import JWTAuth from '@/http/middleware/jwtAuth';
@@ -79,6 +79,7 @@ export default {
*/
editCategory: {
validation: [
param('id').toInt(),
check('name').exists({ checkFalsy: true }).trim().escape(),
check('parent_category_id').optional().isNumeric().toInt(),
check('description').optional().trim().escape(),
@@ -93,13 +94,11 @@ export default {
});
}
const { name, parent_category_id: parentCategoryId, description } = req.body;
const itemCategory = await ItemCategory.where('id', id).fetch();
if (!itemCategory) {
return res.boom.notFound();
}
if (parentCategoryId && parentCategoryId !== itemCategory.attributes.parent_category_id) {
const foundParentCategory = await ItemCategory.where('id', parentCategoryId).fetch();
@@ -109,7 +108,6 @@ export default {
});
}
}
await itemCategory.save({
label: name,
description,
@@ -124,7 +122,9 @@ export default {
* Delete the give item category.
*/
deleteItem: {
validation: [],
validation: [
param('id').toInt(),
],
async handler(req, res) {
const { id } = req.params;
const itemCategory = await ItemCategory.where('id', id).fetch();
@@ -151,4 +151,22 @@ export default {
return res.status(200).send({ items: items.toJSON() });
},
},
getCategory: {
validation: [
param('category_id').toInt(),
],
async handler(req, res) {
const { category_id: categoryId } = req.params;
const item = await ItemCategory.where('id', categoryId).fetch();
if (!item) {
return res.boom.notFound(null, {
errors: [{ type: 'CATEGORY_NOT_FOUND', code: 100 }],
});
}
return res.status(200).send({ category: item.toJSON() });
},
},
};

View File

@@ -0,0 +1,194 @@
import { difference } from 'lodash';
import express from 'express';
import { check, validationResult } from 'express-validator';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Resource from '@/models/Resource';
import View from '../../models/View';
export default {
resource: 'items',
router() {
const router = express.Router();
router.post('/resource/:resource_id',
this.createView.validation,
asyncMiddleware(this.createView.handler));
router.post('/:view_id',
this.editView.validation,
asyncMiddleware(this.editView.handler));
router.delete('/:view_id',
this.deleteView.validation,
asyncMiddleware(this.deleteView.handler));
router.get('/:view_id',
asyncMiddleware(this.getView.handler));
return router;
},
/**
* List all views that associated with the given resource.
*/
listViews: {
validation: [],
async handler(req, res) {
const { resource_id: resourceId } = req.params;
const views = await View.where('resource_id', resourceId).fetchAll();
return res.status(200).send({ views: views.toJSON() });
},
},
getView: {
async handler(req, res) {
const { view_id: viewId } = req.params;
const view = await View.where('id', viewId).fetch({
withRelated: ['resource', 'columns', 'viewRoles'],
});
if (!view) {
return res.boom.notFound(null, {
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
});
}
return res.status(200).send({ ...view.toJSON() });
},
},
/**
* Delete the given view of the resource.
*/
deleteView: {
validation: [],
async handler(req, res) {
const { view_id: viewId } = req.params;
const view = await View.where('id', viewId).fetch({
withRelated: ['viewRoles', 'columns'],
});
if (!view) {
return res.boom.notFound(null, {
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
});
}
if (view.attributes.predefined) {
return res.boom.badRequest(null, {
errors: [{ type: 'PREDEFINED_VIEW', code: 200 }],
});
}
// console.log(view);
await view.destroy();
// await view.columns().destroy({ require: false });
return res.status(200).send({ id: view.get('id') });
},
},
/**
* Creates a new view.
*/
createView: {
validation: [
check('label').exists().escape().trim(),
check('columns').isArray({ min: 3 }),
check('roles').isArray(),
check('roles.*.field').exists().escape().trim(),
check('roles.*.comparator').exists(),
check('roles.*.value').exists(),
check('roles.*.index').exists().isNumeric().toInt(),
],
async handler(req, res) {
const { resource_id: resourceId } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const resource = await Resource.where('id', resourceId).fetch();
if (!resource) {
return res.boom.notFound(null, {
errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }],
});
}
const errorReasons = [];
const { label, roles, columns } = req.body;
const fieldsSlugs = roles.map((role) => role.field);
const resourceFields = await resource.fields().fetch();
const resourceFieldsKeys = resourceFields.map((f) => f.get('key'));
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
if (notFoundFields.length > 0) {
errorReasons.push({ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields });
}
const notFoundColumns = difference(columns, resourceFieldsKeys);
if (notFoundColumns.length > 0) {
errorReasons.push({ type: 'COLUMNS_NOT_EXIST', code: 200, fields: notFoundColumns });
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
const view = await View.forge({
name: label,
predefined: false,
});
// Save view details.
await view.save();
// Save view columns.
// Save view roles.
return res.status(200).send();
},
},
editView: {
validation: [
check('label').exists().escape().trim(),
check('columns').isArray({ min: 3 }),
check('roles').isArray(),
check('roles.*.field').exists().escape().trim(),
check('roles.*.comparator').exists(),
check('roles.*.value').exists(),
check('roles.*.index').exists().isNumeric().toInt(),
],
async handler(req, res) {
const { view_id: viewId } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const view = await View.where('id', viewId).fetch();
if (!view) {
return res.boom.notFound(null, {
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
});
}
return res.status(200).send();
},
},
};

View File

@@ -6,6 +6,8 @@ import Items from '@/http/controllers/Items';
import ItemCategories from '@/http/controllers/ItemCategories';
import Accounts from '@/http/controllers/Accounts';
import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance';
import Views from '@/http/controllers/Views';
import CustomFields from '@/http/controllers/Fields';
export default (app) => {
// app.use('/api/oauth2', OAuth2.router());
@@ -14,6 +16,8 @@ export default (app) => {
app.use('/api/roles', Roles.router());
app.use('/api/accounts', Accounts.router());
app.use('/api/accountOpeningBalance', AccountOpeningBalance.router());
app.use('/api/views', Views.router());
app.use('/api/fields', CustomFields.router());
app.use('/api/items', Items.router());
app.use('/api/item_categories', ItemCategories.router());
};