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

159
server/package-lock.json generated
View File

@@ -858,6 +858,42 @@
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.2.2.tgz",
"integrity": "sha512-18P3VwngjNEcmvPj1mmiHLPyUPjhPAxIyJKDj4PRIY0F5ac3P0Vd0hkASPyWXHK0rfY3P9N2FoxV8ZuYaRBZ1g=="
},
"@sinonjs/commons": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.6.0.tgz",
"integrity": "sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg==",
"dev": true,
"requires": {
"type-detect": "4.0.8"
}
},
"@sinonjs/formatio": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz",
"integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1",
"@sinonjs/samsam": "^3.1.0"
}
},
"@sinonjs/samsam": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz",
"integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.3.0",
"array-from": "^2.1.1",
"lodash": "^4.17.15"
}
},
"@sinonjs/text-encoding": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true
},
"@types/chai": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.0.tgz",
@@ -1231,6 +1267,12 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"array-from": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
"dev": true
},
"array-includes": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz",
@@ -1370,7 +1412,6 @@
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
"dev": true,
"requires": {
"core-js": "^2.4.0",
"regenerator-runtime": "^0.11.0"
@@ -1379,8 +1420,7 @@
"regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
"dev": true
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
}
}
},
@@ -1505,6 +1545,16 @@
"lodash": "^4.17.10"
}
},
"bookshelf-cascade-delete": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/bookshelf-cascade-delete/-/bookshelf-cascade-delete-2.0.1.tgz",
"integrity": "sha1-QRqD48g4lUuSORS9c/rCEnGjrpw=",
"requires": {
"babel-runtime": "^6.6.1",
"bluebird": "^3.3.5",
"lodash": "^4.11.1"
}
},
"bookshelf-json-columns": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bookshelf-json-columns/-/bookshelf-json-columns-2.1.1.tgz",
@@ -2202,8 +2252,7 @@
"core-js": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==",
"dev": true
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
},
"core-js-compat": {
"version": "3.2.1",
@@ -3222,12 +3271,12 @@
}
},
"express-validator": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.1.1.tgz",
"integrity": "sha512-AF6YOhdDiCU7tUOO/OHp2W++I3qpYX7EInMmEEcRGOjs+qoubwgc5s6Wo3OQgxwsWRGCxXlrF73SIDEmY4y3wg==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.2.0.tgz",
"integrity": "sha512-892cPistoSPzMuoG2p1W+2ZxBi0bAvPaaYgXK1E1C8/QncLo2d1HbiDDWkXUtTthjGEzEmwiELLJHu1Ez2hOEg==",
"requires": {
"lodash": "^4.17.11",
"validator": "^11.0.0"
"lodash": "^4.17.15",
"validator": "^11.1.0"
}
},
"extend": {
@@ -3639,13 +3688,11 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"optional": true
"bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -3658,18 +3705,15 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"core-util-is": {
"version": "1.0.2",
@@ -3772,8 +3816,7 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"optional": true
"bundled": true
},
"ini": {
"version": "1.3.5",
@@ -3783,7 +3826,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -3796,20 +3838,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"optional": true
"bundled": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -3826,7 +3865,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -3899,8 +3937,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"object-assign": {
"version": "4.1.1",
@@ -3910,7 +3947,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -4016,7 +4052,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -5083,6 +5118,12 @@
}
}
},
"just-extend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz",
"integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==",
"dev": true
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -5376,6 +5417,12 @@
"resolved": "https://registry.npmjs.org/lodash.result/-/lodash.result-4.5.2.tgz",
"integrity": "sha1-y0Wyf7kU6qjY7m8M57KHC4fLcKo="
},
"lolex": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz",
"integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==",
"dev": true
},
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@@ -5473,6 +5520,11 @@
"mimic-fn": "^1.0.0"
}
},
"memory-cache": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz",
"integrity": "sha1-eJCwHVLADI68nVM+H46xfjA0hxo="
},
"memory-fs": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz",
@@ -5852,6 +5904,36 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"nise": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/nise/-/nise-1.5.2.tgz",
"integrity": "sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==",
"dev": true,
"requires": {
"@sinonjs/formatio": "^3.2.1",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"lolex": "^4.1.0",
"path-to-regexp": "^1.7.0"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"path-to-regexp": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
"dev": true,
"requires": {
"isarray": "0.0.1"
}
}
}
},
"node-libs-browser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@@ -7438,6 +7520,21 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"sinon": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-7.4.2.tgz",
"integrity": "sha512-pY5RY99DKelU3pjNxcWo6XqeB1S118GBcVIIdDi6V+h6hevn1izcg2xv1hTHW/sViRXU7sUOxt4wTUJ3gsW2CQ==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.4.0",
"@sinonjs/formatio": "^3.2.1",
"@sinonjs/samsam": "^3.3.3",
"diff": "^3.5.0",
"lolex": "^4.2.0",
"nise": "^1.5.2",
"supports-color": "^5.5.0"
}
},
"slash": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",

View File

@@ -17,6 +17,7 @@
"@hapi/boom": "^7.4.3",
"bcryptjs": "^2.4.3",
"bookshelf": "^0.15.1",
"bookshelf-cascade-delete": "^2.0.1",
"bookshelf-json-columns": "^2.1.1",
"bookshelf-modelbase": "^2.10.4",
"bookshelf-paranoia": "^0.13.1",

View File

@@ -120,4 +120,51 @@ factory.define('resource', 'resources', () => ({
name: faker.lorem.word(),
}));
factory.define('view', 'views', async () => {
const resource = await factory.create('resource');
return {
name: faker.lorem.word(),
resource_id: resource.id,
predefined: false,
};
});
factory.define('resource_field', 'resource_fields', async () => {
const resource = await factory.create('resource');
const dataTypes = ['select', 'date', 'text'];
return {
label_name: faker.lorem.words(),
data_type: dataTypes[Math.floor(Math.random() * dataTypes.length)],
help_text: faker.lorem.words(),
default: faker.lorem.word(),
resource_id: resource.id,
active: true,
predefined: false,
};
});
factory.define('view_role', 'view_roles', async () => {
const view = await factory.create('view');
const field = await factory.create('resource_field');
return {
view_id: view.id,
index: faker.random.number(),
field_id: field.id,
value: '',
comparator: '',
};
});
factory.define('view_has_columns', 'view_has_columns', async () => {
const view = await factory.create('view');
const field = await factory.create('resource_field');
return {
field_id: field.id,
view_id: view.id,
};
});
export default factory;

View File

@@ -0,0 +1,16 @@
exports.up = function (knex) {
return knex.schema.createTable('resource_fields', (table) => {
table.increments();
table.string('label_name');
table.string('data_type');
table.string('help_text');
table.string('default');
table.boolean('active');
table.boolean('predefined');
table.json('options');
table.integer('resource_id').unsigned().references('id').inTable('resources');
});
};
exports.down = (knex) => knex.schema.dropTableIfExists('resource_fields');

View File

@@ -0,0 +1,10 @@
exports.up = function (knex) {
return knex.schema.createTable('view_has_columns', (table) => {
table.increments();
table.integer('view_id').unsigned();
table.integer('field_id').unsigned();
});
};
exports.down = (knex) => knex.schema.dropTableIfExists('view_has_columns');

View File

@@ -0,0 +1,13 @@
exports.up = function (knex) {
return knex.schema.createTable('view_roles', (table) => {
table.increments();
table.integer('index');
table.integer('field_id').unsigned().references('id').inTable('resource_fields');
table.string('comparator');
table.string('value');
table.integer('view_id').unsigned();
});
};
exports.down = (knex) => knex.schema.dropTableIfExists('view_roles');

View File

@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.createTable('views', (table) => {
table.increments();
table.string('name');
table.boolean('predefined');
table.integer('resource_id').unsigned().references('id').inTable('resources');
});
};
exports.down = (knex) => knex.schema.dropTableIfExists('views');

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

View File

@@ -21,6 +21,11 @@ const Account = bookshelf.Model.extend({
balances() {
return this.hasMany('AccountBalance', 'accounnt_id');
},
}, {
/**
* Cascade delete dependents.
*/
dependents: ['balances'],
});
export default bookshelf.model('Account', Account);

View File

@@ -11,10 +11,18 @@ const Resource = bookshelf.Model.extend({
*/
hasTimestamps: false,
permissions() {
/**
* Resource model may has many views.
*/
views() {
return this.hasMany('View', 'resource_id');
},
roles() {
/**
* Resource model may has many fields.
*/
fields() {
return this.hasMany('ResourceField', 'resource_id');
},
});

View File

@@ -0,0 +1,37 @@
import { snakeCase } from 'lodash';
import bookshelf from './bookshelf';
const ResourceField = bookshelf.Model.extend({
/**
* Table name.
*/
tableName: 'resource_fields',
/**
* Timestamp columns.
*/
hasTimestamps: false,
virtuals: {
/**
* Resource field key.
*/
key() {
return snakeCase(this.attributes.label_name);
},
},
/**
* Resource field may belongs to resource model.
*/
resource() {
return this.belongsTo('Resource', 'resource_id');
},
}, {
/**
* JSON Columns.
*/
jsonColumns: ['options'],
});
export default bookshelf.model('ResourceField', ResourceField);

38
server/src/models/View.js Normal file
View File

@@ -0,0 +1,38 @@
import bookshelf from './bookshelf';
const View = bookshelf.Model.extend({
/**
* Table name.
*/
tableName: 'views',
/**
* Timestamp columns.
*/
hasTimestamps: false,
/**
* View model belongs to resource model.
*/
resource() {
return this.belongsTo('Resource', 'resource_id');
},
/**
* View model may has many columns.
*/
columns() {
return this.belongsToMany('ResourceField', 'view_has_columns', 'view_id', 'field_id');
},
/**
* View model may has many view roles.
*/
viewRoles() {
return this.hasMany('ViewRole', 'view_id');
},
}, {
dependents: ['columns', 'viewRoles'],
});
export default bookshelf.model('View', View);

View File

@@ -0,0 +1,19 @@
import bookshelf from './bookshelf';
const ViewColumn = bookshelf.Model.extend({
/**
* Table name.
*/
tableName: 'view_columns',
/**
* Timestamp columns.
*/
hasTimestamps: false,
view() {
}
});
export default bookshelf.model('ViewColumn', ViewColumn);

View File

@@ -0,0 +1,22 @@
import bookshelf from './bookshelf';
const ViewRole = bookshelf.Model.extend({
/**
* Table name.
*/
tableName: 'view_roles',
/**
* Timestamp columns.
*/
hasTimestamps: false,
/**
* View role model may belongs to view model.
*/
view() {
return this.belongsTo('View', 'view_id');
},
});
export default bookshelf.model('ViewRole', ViewRole);

View File

@@ -2,6 +2,7 @@ import Bookshelf from 'bookshelf';
import jsonColumns from 'bookshelf-json-columns';
import bookshelfParanoia from 'bookshelf-paranoia';
import bookshelfModelBase from 'bookshelf-modelbase';
import cascadeDelete from 'bookshelf-cascade-delete';
import knex from '../database/knex';
const bookshelf = Bookshelf(knex);
@@ -13,5 +14,6 @@ bookshelf.plugin('virtuals');
bookshelf.plugin(jsonColumns);
bookshelf.plugin(bookshelfParanoia);
bookshelf.plugin(bookshelfModelBase.pluggable);
bookshelf.plugin(cascadeDelete);
export default bookshelf;

View File

@@ -0,0 +1,25 @@
import { create, expect } from '~/testInit';
import Resource from '@/models/Resource';
import '@/models/View';
import '@/models/ResourceField';
describe('Model: Resource', () => {
it('Resource model may has many associated views.', async () => {
const view = await create('view');
await create('view', { resource_id: view.resource_id });
const resourceModel = await Resource.where('id', view.resource_id).fetch();
const resourceViews = await resourceModel.views().fetch();
expect(resourceViews).to.have.lengthOf(2);
});
it('Resource model may has many fields.', async () => {
const resourceField = await create('resource_field');
const resourceModel = await Resource.where('id', resourceField.resource_id).fetch();
const resourceFields = await resourceModel.fields().fetch();
expect(resourceFields).to.have.lengthOf(1);
});
});

View File

@@ -0,0 +1,18 @@
import { create, expect } from '~/testInit';
import Resource from '@/models/Resource';
import ResourceField from '@/models/ResourceField';
import '@/models/View';
describe('Model: ResourceField', () => {
it('Resource field model may belongs to associated resource.', async () => {
const resourceField = await create('resource_field');
const resourceFieldModel = await ResourceField.where('id', resourceField.id).fetch();
const resourceModel = resourceFieldModel.resource().fetch();
const foundResource = await Resource.where('id', resourceField.resource_id).fetch();
expect(resourceModel.attributes.id).equals(foundResource.id);
expect(resourceModel.attributes.name).equals(foundResource.name);
});
});

View File

@@ -0,0 +1,42 @@
import { create, expect } from '~/testInit';
import View from '@/models/View';
import Resource from '@/models/Resource';
import '@/models/ResourceField';
import '@/models/ViewRole';
describe('Model: View', () => {
it('View model may has many associated resource.', async () => {
const view = await create('view');
const viewModel = await View.where('id', view.id).fetch();
const viewResource = await viewModel.resource().fetch();
const foundResource = await Resource.where('id', view.resource_id).fetch();
expect(viewResource.attributes.id).equals(foundResource.id);
expect(viewResource.attributes.name).equals(foundResource.attributes.name);
});
it('View model may has many associated view roles.', async () => {
const view = await create('view');
await create('view_role', { view_id: view.id });
await create('view_role', { view_id: view.id });
const viewModel = await View.where('id', view.id).fetch();
const viewRoles = await viewModel.viewRoles().fetch();
expect(viewRoles).to.have.lengthOf(2);
});
it('View model may has many associated view columns', async () => {
const view = await create('view');
await create('view_has_columns', { view_id: view.id });
await create('view_has_columns', { view_id: view.id });
const viewModel = await View.where('id', view.id).fetch();
const viewColumns = await viewModel.columns().fetch();
expect(viewColumns).to.have.lengthOf(2);
});
});

View File

@@ -0,0 +1,215 @@
import { create, expect, request } from '~/testInit';
import knex from '@/database/knex';
describe('route: `/fields`', () => {
describe('POST: `/fields/:resource_id`', () => {
it('Should `label` be required.', async () => {
const resource = await create('resource');
const res = await request().post(`/api/fields/resource/${resource.resource_id}`).send();
expect(res.status).equals(422);
expect(res.body.code).equals('VALIDATION_ERROR');
const paramsErrors = res.body.errors.map((er) => er.param);
expect(paramsErrors).to.include('label');
});
it('Should `data_type` be required.', async () => {
const resource = await create('resource');
const res = await request().post(`/api/fields/resource/${resource.resource_id}`);
expect(res.status).equals(422);
expect(res.body.code).equals('VALIDATION_ERROR');
const paramsErrors = res.body.errors.map((er) => er.param);
expect(paramsErrors).to.include('data_type');
});
it('Should `data_type` be one in the list.', async () => {
const resource = await create('resource');
const res = await request().post(`/api/fields/resource/${resource.resource_id}`).send({
label: 'Field label',
data_type: 'invalid_type',
});
expect(res.status).equals(422);
expect(res.body.code).equals('VALIDATION_ERROR');
const paramsErrors = res.body.errors.map((er) => er.param);
expect(paramsErrors).to.include('data_type');
});
it('Should `value` be boolean valid value in case `data_type` was `boolean`.', () => {
});
it('Should `value` be URL valid value in case `data_type` was `url`.', () => {
});
it('Should `value` be integer valid value in case `data_type` was `number`.', () => {
});
it('Should `value` be decimal valid value in case `data_type` was `decimal`.', () => {
});
it('Should `value` be email valid value in case `data_type` was `email`.', () => {
});
it('Should `value` be boolean valid value in case `data_type` was `checkbox`.', () => {
});
it('Should response not found in case resource id was not exist.', async () => {
const res = await request().post('/api/fields/resource/100').send({
label: 'Field label',
data_type: 'text',
default: 'default value',
help_text: 'help text',
});
expect(res.status).equals(404);
expect(res.body.errors).include.something.that.deep.equals({
type: 'RESOURCE_NOT_FOUND', code: 100,
});
});
it('Should response success with valid data.', async () => {
const resource = await create('resource');
const res = await request().post(`/api/fields/resource/${resource.id}`).send({
label: 'Field label',
data_type: 'text',
default: 'default value',
help_text: 'help text',
});
expect(res.status).equals(200);
});
it('Should store the given field details to the storage.', async () => {
const resource = await create('resource');
await request().post(`/api/fields/resource/${resource.id}`).send({
label: 'Field label',
data_type: 'text',
default: 'default value',
help_text: 'help text',
options: ['option 1', 'option 2'],
});
const foundField = await knex('resource_fields').first();
expect(foundField.label_name).equals('Field label');
expect(foundField.data_type).equals('text');
expect(foundField.default).equals('default value');
expect(foundField.help_text).equals('help text');
expect(foundField.options).equals.deep([
{ key: 1, value: 'option 1' },
{ key: 2, value: 'option 2' },
]);
});
});
describe('POST: `/fields/:field_id`', () => {
it('Should `label` be required.', async () => {
const field = await create('resource_field');
const res = await request().post(`/api/fields/${field.id}`).send();
expect(res.status).equals(422);
expect(res.body.code).equals('VALIDATION_ERROR');
const paramsErrors = res.body.errors.map((er) => er.param);
expect(paramsErrors).to.include('label');
});
it('Should `data_type` be required.', async () => {
const field = await create('resource_field');
const res = await request().post(`/api/fields/${field.id}`);
expect(res.status).equals(422);
expect(res.body.code).equals('VALIDATION_ERROR');
const paramsErrors = res.body.errors.map((er) => er.param);
expect(paramsErrors).to.include('data_type');
});
it('Should `data_type` be one in the list.', async () => {
const field = await create('resource_field');
const res = await request().post(`/api/fields/${field.id}`).send({
label: 'Field label',
data_type: 'invalid_type',
});
expect(res.status).equals(422);
expect(res.body.code).equals('VALIDATION_ERROR');
const paramsErrors = res.body.errors.map((er) => er.param);
expect(paramsErrors).to.include('data_type');
});
it('Should response not found in case resource id was not exist.', async () => {
const res = await request().post('/api/fields/100').send({
label: 'Field label',
data_type: 'text',
default: 'default value',
help_text: 'help text',
});
expect(res.status).equals(404);
expect(res.body.errors).include.something.that.deep.equals({
type: 'FIELD_NOT_FOUND', code: 100,
});
});
it('Should save the new options of the field in the storage.', async () => {
});
});
describe('POST: `/fields/status/:field_id`', () => {
it('Should response not found in case field id was not exist.', async () => {
const res = await request().post('/api/fields/status/100').send();
expect(res.status).equals(404);
});
it('Should change status activation of the given field.', async () => {
const field = await create('resource_field');
await request().post(`/api/fields/status/${field.id}`).send({
active: false,
});
const storedField = await knex('resource_fields').where('id', field.id).first();
expect(storedField.active).equals(0);
});
});
describe('DELETE: `/fields/:field_id`', () => {
it('Should response not found in case field id was not exist.', async () => {
const res = await request().delete('/api/fields/100').send();
expect(res.status).equals(404);
});
it('Should not delete predefined field.', async () => {
const field = await create('resource_field', { predefined: true });
const res = await request().delete(`/api/fields/${field.id}`).send();
expect(res.status).equals(400);
expect(res.body.errors).include.something.that.deep.equals({
type: 'PREDEFINED_FIELD', code: 100,
});
});
it('Should delete the given field from the storage.', async () => {
const field = await create('resource_field');
const res = await request().delete(`/api/fields/${field.id}`).send();
expect(res.status).equals(200);
});
});
});

View File

@@ -1,7 +1,7 @@
import { request, expect, create } from '~/testInit';
import knex from '@/database/knex';
describe.only('routes: `/roles/`', () => {
describe('routes: `/roles/`', () => {
describe('POST: `/roles/`', () => {
it('Should `name` be required.', async () => {
const res = await request().post('/api/roles').send();
@@ -237,7 +237,7 @@ describe.only('routes: `/roles/`', () => {
expect(storedResources).to.have.lengthOf(1);
});
it.only('Should save the submit permissions in the storage in case was not exist.', async () => {
it('Should save the submit permissions in the storage in case was not exist.', async () => {
const role = await create('role');
await request().post(`/api/roles/${role.id}`).send({
name: 'Role Name',

View File

@@ -0,0 +1,310 @@
import { request, expect, create } from '~/testInit';
import View from '@/models/View';
import ViewRole from '@/models/ViewRole';
import '@/models/ResourceField';
describe('routes: `/views`', () => {
describe('POST: `/views/:resource_id`', () => {
it('Should `label` be required.', async () => {
const resource = await create('resource');
const res = await request().post(`/api/views/resource/${resource.id}`);
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('label');
});
it('Should columns be minimum limited', async () => {
const resource = await create('resource');
const res = await request().post(`/api/views/resource/${resource.id}`, {
label: 'View Label',
columns: [],
});
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('columns');
});
it('Should columns be array.', async () => {
const resource = await create('resource');
const res = await request().post(`/api/views/resource/${resource.id}`, {
label: 'View Label',
columns: 'not_array',
});
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('columns');
});
it('Should `roles.*.field` be required.', async () => {
const resource = await create('resource');
const res = await request().post(`/api/views/resource/${resource.id}`).send({
label: 'View Label',
roles: [{}],
});
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('roles[0].field');
});
it('Should `roles.*.comparator` be valid.', async () => {
const resource = await create('resource');
const res = await request().post(`/api/views/resource/${resource.id}`).send({
label: 'View Label',
roles: [{}],
});
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('roles[0].comparator');
});
it('Should `roles.*.index` be number as integer.', async () => {
const resource = await create('resource');
const res = await request().post(`/api/views/resource/${resource.id}`).send({
label: 'View Label',
roles: [{ index: 'not_numeric' }],
});
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('roles[0].index');
});
it('Should response not found in case resource was not exist.', async () => {
const res = await request().post('/api/views/resource/100').send({
label: 'View Label',
columns: ['amount', 'thumbnail', 'status'],
roles: [{
index: 1,
field: 'amount',
comparator: 'equals',
value: '100',
}],
});
expect(res.status).equals(404);
});
it('Should response the roles fields not exist in case role field was not exist.', async () => {
const resource = await create('resource');
await create('resource_field', {
resource_id: resource.id,
label_name: 'Amount',
});
const res = await request().post(`/api/views/resource/${resource.id}`).send({
label: 'View Label',
columns: ['amount', 'thumbnail', 'status'],
roles: [{
index: 1,
field: 'price',
comparator: 'equals',
value: '100',
}],
});
expect(res.body.errors).include.something.that.deep.equals({
type: 'RESOURCE_FIELDS_NOT_EXIST',
code: 100,
fields: ['price'],
});
});
it('Should response the columns not exists in case column was not exist.', async () => {
const resource = await create('resource');
await create('resource_field', {
resource_id: resource.id,
label_name: 'Amount',
});
const res = await request().post(`/api/views/resource/${resource.id}`).send({
label: 'View Label',
columns: ['amount', 'thumbnail', 'status'],
roles: [{
index: 1,
field: 'price',
comparator: 'equals',
value: '100',
}],
});
expect(res.body.errors).include.something.that.deep.equals({
type: 'COLUMNS_NOT_EXIST',
code: 200,
fields: ['thumbnail', 'status'],
});
});
it('Should save the given details with associated roles and columns.', async () => {
});
});
describe.only('POST: `/views/:view_id`', () => {
it('Should `label` be required.', async () => {
const view = await create('view');
const res = await request().post(`/api/views/${view.id}`);
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('label');
});
it('Should columns be minimum limited', async () => {
const view = await create('view');
const res = await request().post(`/api/views/${view.id}`, {
label: 'View Label',
columns: [],
});
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('columns');
});
it('Should columns be array.', async () => {
const view = await create('view');
const res = await request().post(`/api/views/${view.id}`, {
label: 'View Label',
columns: 'not_array',
});
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('columns');
});
it('Should `roles.*.field` be required.', async () => {
const view = await create('view');
const res = await request().post(`/api/views/${view.id}`).send({
label: 'View Label',
roles: [{}],
});
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('roles[0].field');
});
it('Should `roles.*.comparator` be valid.', async () => {
const view = await create('view');
const res = await request().post(`/api/views/${view.id}`).send({
label: 'View Label',
roles: [{}],
});
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('roles[0].comparator');
});
it('Should `roles.*.index` be number as integer.', async () => {
const view = await create('view');
const res = await request().post(`/api/views/${view.id}`).send({
label: 'View Label',
roles: [{ index: 'not_numeric' }],
});
expect(res.status).equals(422);
expect(res.body.code).equals('validation_error');
const paramsErrors = res.body.errors.map((error) => error.param);
expect(paramsErrors).to.include('roles[0].index');
});
it('Should response not found in case resource was not exist.', async () => {
const res = await request().post('/api/views/100').send({
label: 'View Label',
columns: ['amount', 'thumbnail', 'status'],
roles: [{
index: 1,
field: 'amount',
comparator: 'equals',
value: '100',
}],
});
expect(res.status).equals(404);
});
it.only('Should response the roles fields not exist in case role field was not exist.', async () => {
const view = await create('view');
await create('resource_field', {
resource_id: view.resource_id,
label_name: 'Amount',
});
const res = await request().post(`/api/views/${view.id}`).send({
label: 'View Label',
columns: ['amount', 'thumbnail', 'status'],
roles: [{
index: 1,
field: 'price',
comparator: 'equals',
value: '100',
}],
});
expect(res.body.errors).include.something.that.deep.equals({
type: 'RESOURCE_FIELDS_NOT_EXIST',
code: 100,
fields: ['price'],
});
});
});
describe('DELETE: `/views/:resource_id`', () => {
it('Should not delete predefined view.', async () => {
const view = await create('view', { predefined: true });
const res = await request().delete(`/api/views/${view.id}`).send();
expect(res.status).equals(400);
});
it('Should response not found in case view was not exist.', async () => {
const res = await request().delete('/api/views/100').send();
expect(res.status).equals(404);
expect(res.body.errors).include.something.that.deep.equals({
type: 'VIEW_NOT_FOUND', code: 100,
});
});
it('Should delete the given view and associated view columns and roles.', async () => {
const view = await create('view', { predefined: false });
await create('view_role', { view_id: view.id });
await create('view_has_columns', { view_id: view.id });
await request().delete(`/api/views/${view.id}`).send();
const foundViews = await View.where('id', view.id).fetchAll();
const foundViewRoles = await ViewRole.where('view_id', view.id).fetchAll();
expect(foundViews).to.have.lengthOf(0);
expect(foundViewRoles).to.have.lengthOf(0);
});
});
});