From ff0a26a790efdb79b720f900b153b117821595e4 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 15 Apr 2020 20:13:55 +0200 Subject: [PATCH] fix api global options. --- server/src/data/options.js | 38 ++++++ server/src/http/controllers/Options.js | 40 +++++-- server/src/lib/Metable/MetableCollection.js | 40 +++++-- server/src/models/Option.js | 15 +++ server/tests/lib/MetableCollection.test.js | 48 ++++++-- server/tests/routes/accounting.test.js | 2 +- server/tests/routes/options.test.js | 123 ++++++++++++++++++++ 7 files changed, 281 insertions(+), 25 deletions(-) create mode 100644 server/src/data/options.js diff --git a/server/src/data/options.js b/server/src/data/options.js new file mode 100644 index 000000000..e2924d59b --- /dev/null +++ b/server/src/data/options.js @@ -0,0 +1,38 @@ + + +export default { + organization: [ + { + key: 'name', + type: 'string', + }, + { + key: 'base_currency', + type: 'string', + }, + { + key: 'industry', + type: 'string', + }, + { + key: 'location', + type: 'string', + }, + { + key: 'fiscal_year', + type: 'string', + }, + { + key: 'language', + type: 'string', + }, + { + key: 'time_zone', + type: 'string', + }, + { + key: 'date_format', + type: 'string', + }, + ], +}; \ No newline at end of file diff --git a/server/src/http/controllers/Options.js b/server/src/http/controllers/Options.js index 4ef40dee1..5792a83d5 100644 --- a/server/src/http/controllers/Options.js +++ b/server/src/http/controllers/Options.js @@ -1,7 +1,9 @@ import express from 'express'; import { body, query, validationResult } from 'express-validator'; +import { pick } from 'lodash'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import Option from '@/models/Option'; +import jwtAuth from '@/http/middleware/jwtAuth'; export default { /** @@ -10,13 +12,15 @@ export default { router() { const router = express.Router(); + router.use(jwtAuth); + router.post('/', this.saveOptions.validation, asyncMiddleware(this.saveOptions.handler)); router.get('/', this.getOptions.validation, - asyncMiddleware(this.getSettings)); + asyncMiddleware(this.getOptions.handler)); return router; }, @@ -26,7 +30,7 @@ export default { */ saveOptions: { validation: [ - body('options').isArray(), + body('options').isArray({ min: 1 }), body('options.*.key').exists(), body('options.*.value').exists(), body('options.*.group').exists(), @@ -42,12 +46,25 @@ export default { const form = { ...req.body }; const optionsCollections = await Option.query(); + const errorReasons = []; + const notDefinedOptions = Option.validateDefined(form.options); + + if (notDefinedOptions.length) { + errorReasons.push({ + type: 'OPTIONS.KEY.NOT.DEFINED', + code: 200, + keys: notDefinedOptions.map(o => ({ ...pick(o, ['key', 'group']) })), + }); + } + if (errorReasons.length) { + return res.status(400).send({ errors: errorReasons }); + } form.options.forEach((option) => { - optionsCollections.setMeta(option.key, option.value, option.group); + optionsCollections.setMeta({ ...option }); }); await optionsCollections.saveMeta(); - return res.status(200).send(); + return res.status(200).send({ options: form }); }, }, @@ -57,6 +74,7 @@ export default { getOptions: { validation: [ query('key').optional(), + query('group').optional(), ], async handler(req, res) { const validationErrors = validationResult(req); @@ -66,9 +84,17 @@ export default { code: 'VALIDATION_ERROR', ...validationErrors, }); } - const options = await Option.query(); - - return res.status(200).sends({ options }); + const filter = { ...req.query }; + const options = await Option.query().onBuild((builder) => { + if (filter.key) { + builder.where('key', filter.key); + } + if (filter.group) { + builder.where('group', filter.group); + } + }); + + return res.status(200).send({ options: options.metadata }); }, }, }; diff --git a/server/src/lib/Metable/MetableCollection.js b/server/src/lib/Metable/MetableCollection.js index 7e2e0eda0..f6e5a31fb 100644 --- a/server/src/lib/Metable/MetableCollection.js +++ b/server/src/lib/Metable/MetableCollection.js @@ -1,5 +1,8 @@ export default class MetableCollection { + /** + * Constructor method. + */ constructor() { this.metadata = []; this.KEY_COLUMN = 'key'; @@ -21,13 +24,29 @@ export default class MetableCollection { this.model = model; } + /** + * Sets a extra columns. + * @param {Array} columns - + */ + setExtraColumns(columns) { + this.extraColumns = columns; + } + /** * Find the given metadata key. * @param {String} key - * @return {object} - Metadata object. */ - findMeta(key) { - return this.allMetadata().find((meta) => meta.key === key); + findMeta(payload) { + const { key, extraColumns } = this.parsePayload(payload); + + return this.allMetadata().find((meta) => { + const isSameKey = meta.key === key; + const sameExtraColumns = this.extraColumns.some((extraColumn) => { + return !extraColumns || (extraColumns[extraColumn] === meta[extraColumn]); + }); + return isSameKey && sameExtraColumns; + }); } /** @@ -42,8 +61,8 @@ export default class MetableCollection { * @param {String} key - * @param {Mixied} defaultValue - */ - getMeta(key, defaultValue) { - const metadata = this.findMeta(key); + getMeta(payload, defaultValue) { + const metadata = this.findMeta(payload); return metadata ? metadata.value : defaultValue || false; } @@ -79,7 +98,7 @@ export default class MetableCollection { * @param {String} key - * @param {String} value - */ - setMeta(key, value, payload) { + setMeta(payload, ...args) { if (Array.isArray(key)) { const metadata = key; @@ -88,18 +107,23 @@ export default class MetableCollection { }); return; } - const metadata = this.findMeta(key); + const { key, value, ...extraColumns } = this.parsePayload(payload, args[0]); + const metadata = this.findMeta(payload); if (metadata) { metadata.value = value; metadata.markAsUpdated = true; } else { this.metadata.push({ - value, key, ...payload, markAsInserted: true, + value, key, ...extraColumns, markAsInserted: true, }); } } + parsePayload(payload, value) { + return typeof payload !== 'object' ? { key: payload, value } : { ...payload }; + } + /** * Saved the modified/deleted and inserted metadata. */ @@ -111,7 +135,7 @@ export default class MetableCollection { if (deleted.length > 0) { deleted.forEach((meta) => { - const deleteOper = this.model.query().beforeRun((query, result) => { + const deleteOper = this.model.query().onBuild((query, result) => { this.extraQuery(query, meta); return result; }).delete(); diff --git a/server/src/models/Option.js b/server/src/models/Option.js index 295ec635a..ff8c033ea 100644 --- a/server/src/models/Option.js +++ b/server/src/models/Option.js @@ -1,6 +1,7 @@ import { mixin } from 'objection'; import BaseModel from '@/models/Model'; import MetableCollection from '@/lib/Metable/MetableCollection'; +import definedOptions from '@/data/options'; export default class Option extends mixin(BaseModel, [mixin]) { /** @@ -18,6 +19,7 @@ export default class Option extends mixin(BaseModel, [mixin]) { return super.query(...args).runAfter((result) => { if (result instanceof MetableCollection) { result.setModel(Option); + result.setExtraColumns(['group']); } return result; }); @@ -26,4 +28,17 @@ export default class Option extends mixin(BaseModel, [mixin]) { static get collection() { return MetableCollection; } + + static validateDefined(options) { + const notDefined = []; + + options.forEach((option) => { + if (!definedOptions[option.group]) { + notDefined.push(option); + } else if (!definedOptions[option.group].some((o) => o.key === option.key)) { + notDefined.push(option); + } + }); + return notDefined; + } } diff --git a/server/tests/lib/MetableCollection.test.js b/server/tests/lib/MetableCollection.test.js index a3abba9dd..e1d5cdc01 100644 --- a/server/tests/lib/MetableCollection.test.js +++ b/server/tests/lib/MetableCollection.test.js @@ -12,6 +12,17 @@ describe('MetableCollection', () => { const foundMeta = metadataCollection.findMeta(option.key); expect(foundMeta).to.be.an('object'); }); + + it('Should retrieve the found meta with extra columns.', async () => { + const option = await create('option'); + const metadataCollection = await Option.query(); + + const foundMeta = metadataCollection.findMeta({ + key: option.key, + group: option.group, + }); + expect(foundMeta).to.be.an('object'); + }); }); describe('allMetadata', () => { @@ -49,6 +60,21 @@ describe('MetableCollection', () => { expect(metadataCollection.metadata.length).equals(1); }); + + it('Should sets the meta value with extra columns', async () => { + const metadataCollection = new MetadataCollection(); + metadataCollection.setMeta({ + key: 'key', + value: 'value', + group: 'group-1', + }); + + expect(metadataCollection.metadata.length).equals(1); + + expect(metadataCollection.metadata[0].key).equals('key'); + expect(metadataCollection.metadata[0].value).equals('value'); + expect(metadataCollection.metadata[0].group).equals('group-1'); + }); }); describe('removeAllMeta()', () => { @@ -82,7 +108,7 @@ describe('MetableCollection', () => { it('Should save updated the exist metadata.', async () => { const option = await create('option'); - const metadataCollection = new MetadataCollection(); + const metadataCollection = await Option.query(); metadataCollection.setModel(Option); metadataCollection.setMeta(option.key, 'value'); @@ -90,23 +116,24 @@ describe('MetableCollection', () => { await metadataCollection.saveMeta(); - const storedMetadata = await Option.query().where('key', option.key).first(); - expect(storedMetadata.value).equals('value'); + const storedMetadata = await Option.query().where('key', option.key); + + expect(storedMetadata.metadata[0].value).equals('value'); + expect(storedMetadata.metadata[0].key).equals(option.key); + expect(storedMetadata.metadata[0].group).equals(option.group); }); it('Should delete the removed metadata from storage.', async () => { const option = await create('option'); - const options = await Option.query(); - const metadataCollection = MetadataCollection.from(options); - metadataCollection.setModel(Option); + const metadataCollection = await Option.query(); metadataCollection.removeMeta(option.key); expect(metadataCollection.metadata.length).equals(1); await metadataCollection.saveMeta(); const storedMetadata = await Option.query(); - expect(storedMetadata.length).equals(0); + expect(storedMetadata.metadata.length).equals(0); }); it('Should save instered new metadata with extra columns.', async () => { @@ -116,8 +143,11 @@ describe('MetableCollection', () => { metadataCollection.extraColumns = ['resource_id']; metadataCollection.setModel(ResourceFieldMetadata); - metadataCollection.setMeta('key', 'value', { resource_id: resource.id }); - + metadataCollection.setMeta({ + key: 'key', + value: 'value', + resource_id: resource.id, + }); await metadataCollection.saveMeta(); const storedMetadata = await ResourceFieldMetadata.query().first(); diff --git a/server/tests/routes/accounting.test.js b/server/tests/routes/accounting.test.js index 43c8d3b53..7dc6f94c5 100644 --- a/server/tests/routes/accounting.test.js +++ b/server/tests/routes/accounting.test.js @@ -608,7 +608,7 @@ describe('routes: `/accounting`', () => { }); }); - describe.only('route: POST `accounting/manual-journals/:id/publish`', () => { + describe('route: POST `accounting/manual-journals/:id/publish`', () => { it('Should response not found in case the manual journal id was not exists.', async () => { const manualJournal = await create('manual_journal'); diff --git a/server/tests/routes/options.test.js b/server/tests/routes/options.test.js index e69de29bb..93f1791e3 100644 --- a/server/tests/routes/options.test.js +++ b/server/tests/routes/options.test.js @@ -0,0 +1,123 @@ +import knex from '@/database/knex'; +import { + request, + expect, + create, + make, + login, +} from '~/testInit'; +import Option from '@/models/Option'; + +let loginRes; + +describe('routes: `/options`', () => { + beforeEach(async () => { + loginRes = await login(); + }); + afterEach(() => { + loginRes = null; + }); + + describe('POST: `/options/`', () => { + it('Should response unauthorized if the user was not logged in.', async () => { + const res = await request() + .post('/api/options') + .send(); + + expect(res.status).equals(401); + expect(res.body.message).equals('unauthorized'); + }); + + it('Should response the options key and group is not defined.', async () => { + const res = await request() + .post('/api/options') + .set('x-access-token', loginRes.body.token) + .send({ + options: [ + { + key: 'key', + value: 'hello world', + group: 'group', + }, + ], + }); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'OPTIONS.KEY.NOT.DEFINED', + code: 200, + keys: [ + { key: 'key', group: 'group' }, + ], + }); + }); + + it('Should save options to the storage.', async () => { + const res = await request() + .post('/api/options') + .set('x-access-token', loginRes.body.token) + .send({ + options: [{ + key: 'name', + group: 'organization', + value: 'hello world', + }], + }); + expect(res.status).equals(200); + + const storedOptions = await Option.query() + .where('group', 'organization') + .where('key', 'name'); + + expect(storedOptions.metadata.length).equals(1); + }); + }); + + describe('GET: `/options`', () => { + it('Should response unauthorized if the user was not unauthorized.', async () => { + const res = await request() + .get('/api/options') + .query({ + group: 'organization', + }) + .send(); + + expect(res.status).equals(401); + expect(res.body.message).equals('unauthorized'); + }); + + it('Should retrieve options the associated to the given group.', async () => { + await create('option', { group: 'organization', key: 'name' }); + await create('option', { group: 'organization', key: 'base_currency' }); + + const res = await request() + .get('/api/options') + .set('x-access-token', loginRes.body.token) + .query({ + group: 'organization', + }) + .send(); + + expect(res.status).equals(200); + expect(res.body.options).is.an('array'); + expect(res.body.options.length).equals(2); + }); + + it('Should retrieve options that associated to the given key.', async () => { + await create('option', { group: 'organization', key: 'base_currency' }); + await create('option', { group: 'organization', key: 'name' }); + + const res = await request() + .get('/api/options') + .set('x-access-token', loginRes.body.token) + .query({ + key: 'name', + }) + .send(); + + expect(res.status).equals(200); + expect(res.body.options).is.an('array'); + expect(res.body.options.length).equals(1); + }); + }); +}); \ No newline at end of file