WIP Custom fields feature.

This commit is contained in:
Ahmed Bouhuolia
2020-01-26 21:14:21 +02:00
parent 77c67cc4cb
commit a97b527e8c
10 changed files with 326 additions and 75 deletions

View File

@@ -0,0 +1,14 @@
import MetableCollection from '@/lib/Metable/MetableCollection';
import ResourceFieldMetadata from '@/models/ResourceFieldMetadata';
export default class ResourceFieldMetadataCollection extends MetableCollection {
/**
* Constructor method.
*/
constructor() {
super();
this.setModel(ResourceFieldMetadata);
this.extraColumns = ['resource_id', 'resource_item_id'];
}
}

View File

@@ -6,7 +6,7 @@ import {
validationResult,
} from 'express-validator';
import moment from 'moment';
import { difference, chain } from 'lodash';
import { difference, chain, omit } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Expense from '@/models/Expense';
import Account from '@/models/Account';
@@ -50,6 +50,10 @@ export default {
this.listExpenses.validation,
asyncMiddleware(this.listExpenses.handler));
router.get('/:id',
this.getExpense.validation,
asyncMiddleware(this.getExpense.handler));
return router;
},
@@ -81,11 +85,12 @@ export default {
const form = {
date: new Date(),
published: false,
custom_fields: [],
...req.body,
};
// 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();
@@ -98,18 +103,19 @@ export default {
if (!expenseAccount) {
errorReasons.push({ type: 'EXPENSE.ACCOUNT.NOT.FOUND', code: 200 });
}
const customFields = new ResourceCustomFieldRepository('Expense');
const customFields = new ResourceCustomFieldRepository(Expense);
await customFields.load();
customFields.fillCustomFields(form.custom_fields);
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({ ...form });
const expenseTransaction = await Expense.query().insertAndFetch({
...omit(form, ['custom_fields']),
});
customFields.fillCustomFields(expenseTransaction.id, form.custom_fields);
const journalEntries = new JournalPoster();
const creditEntry = new JournalEntry({
@@ -433,4 +439,43 @@ export default {
}
},
},
/**
* Retrieve details of the given expense id.
*/
getExpense: {
validation: [
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { id } = req.params;
const expenseTransaction = await Expense.query().findById(id);
if (!expenseTransaction) {
return res.status(404).send({
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
});
}
const expenseCFMetadataRepo = new ResourceCustomFieldRepository(Expense);
await expenseCFMetadataRepo.load();
await expenseCFMetadataRepo.fetchCustomFieldsMetadata(expenseTransaction.id);
const expenseCusFieldsMetadata = expenseCFMetadataRepo.getMetadata(expenseTransaction.id);
return res.status(200).send({
...expenseTransaction,
custom_fields: [
...expenseCusFieldsMetadata.toArray(),
],
});
},
},
};

View File

@@ -6,6 +6,11 @@ export default class MetableCollection {
this.VALUE_COLUMN = 'value';
this.TYPE_COLUMN = 'type';
this.model = null;
this.extraColumns = [];
this.extraQuery = (query, meta) => {
query.where('key', meta[this.KEY_COLUMN]);
};
}
/**
@@ -59,9 +64,14 @@ export default class MetableCollection {
* @param {*} group
*/
removeAllMeta(group = 'default') {
this.metadata.forEach(meta => {
meta.markAsDeleted = true;
});
this.metadata = this.metadata.map((meta) => ({
...meta,
markAsDeleted: true,
}));
}
setExtraQuery(callback) {
this.extraQuery = callback;
}
/**
@@ -100,18 +110,35 @@ export default class MetableCollection {
const opers = [];
if (deleted.length > 0) {
const deleteOper = this.model.query()
.whereIn('key', deleted.map((meta) => meta.key)).delete();
opers.push(deleteOper);
deleted.forEach((meta) => {
const deleteOper = this.model.query().beforeRun((query, result) => {
this.extraQuery(query, meta);
return result;
}).delete();
opers.push(deleteOper);
});
}
inserted.forEach((meta) => {
const insertOper = this.model.query().insert({
[this.KEY_COLUMN]: meta.key,
[this.VALUE_COLUMN]: meta.value,
...this.extraColumns.reduce((obj, column) => {
if (typeof meta[column] !== 'undefined') {
obj[column] = meta[column];
}
return obj;
}, {}),
});
opers.push(insertOper);
});
updated.forEach((meta) => {
const updateOper = this.model.query().onBuild((query) => {
this.extraQuery(query, meta);
}).patch({
[this.VALUE_COLUMN]: meta.value,
});
opers.push(updateOper);
});
await Promise.all(opers);
}
@@ -198,6 +225,11 @@ export default class MetableCollection {
this.metadata.push(meta);
}
toArray() {
return this.metadata;
}
/**
* Static method to load metadata to the collection.
* @param {Array} meta

View File

@@ -1,7 +1,6 @@
import { Model } from 'objection';
export default class ModelBase extends Model {
static get collection() {
return Array;
}

View File

@@ -10,6 +10,19 @@ export default class Option extends mixin(BaseModel, [mixin]) {
return 'options';
}
/**
* Override the model query.
* @param {...any} args -
*/
static query(...args) {
return super.query(...args).runAfter((result) => {
if (result instanceof MetableCollection) {
result.setModel(Option);
}
return result;
});
}
static get collection() {
return MetableCollection;
}

View File

@@ -1,7 +1,7 @@
import { Model } from 'objection';
import path from 'path';
import BaseModel from '@/models/Model';
import MetableCollection from '@/lib/Metable/MetableCollection';
import ResourceFieldMetadataCollection from '@/collection/ResourceFieldMetadataCollection';
export default class ResourceFieldMetadata extends BaseModel {
/**
@@ -15,7 +15,7 @@ export default class ResourceFieldMetadata extends BaseModel {
* Override the resource field metadata collection.
*/
static get collection() {
return MetableCollection;
return ResourceFieldMetadataCollection;
}
/**

View File

@@ -1,23 +1,24 @@
import Resource from '@/models/Resource';
import ResourceField from '@/models/ResourceField';
import ResourceFieldMetadata from '@/models/ResourceFieldMetadata';
import ModelBase from '@/models/Model';
import ResourceFieldMetadataCollection from '@/collection/ResourceFieldMetadataCollection';
export default class ResourceCustomFieldRepository {
/**
* Class constructor.
*/
constructor(model) {
if (typeof model === 'function') {
this.resourceName = model.name;
} else if (typeof model === 'string') {
this.resourceName = model;
}
// Custom fields of the given resource.
this.customFields = [];
this.filledCustomFields = [];
this.filledCustomFields = {};
// metadata of custom fields of the given resource.
this.metadata = {};
this.fieldsMetadata = {};
this.resource = {};
}
@@ -36,7 +37,7 @@ export default class ResourceCustomFieldRepository {
.where('resource_id', this.resource.id)
.where('resource_item_id', id);
this.metadata[id] = metadata;
this.fieldsMetadata[id] = metadata;
}
/**
@@ -86,21 +87,49 @@ export default class ResourceCustomFieldRepository {
* @param {Integer} itemId -
*/
getMetadata(itemId) {
return this.metadata[itemId] || this.metadata;
return this.fieldsMetadata[itemId] || this.fieldsMetadata;
}
/**
* Fill metadata of the custom fields that associated to the resource.
* @param {Inter} id - Resource item id.
* @param {Array} attributes -
*/
fillCustomFields(id, attributes) {
if (typeof this.filledCustomFields[id] === 'undefined') {
this.filledCustomFields[id] = [];
}
attributes.forEach((attr) => {
this.filledCustomFields[id].push(attr);
this.fieldsMetadata[id].setMeta(attr.key, attr.value);
if (!this.fieldsMetadata[id]) {
this.fieldsMetadata[id] = new ResourceFieldMetadataCollection();
}
this.fieldsMetadata[id].setMeta(attr.key, attr.value, {
resource_id: this.resource.id,
resource_item_id: id,
});
});
}
saveCustomFields(id) {
this.fieldsMetadata.saveMeta();
/**
* Saves the instered, updated and deleted custom fields metadata.
* @param {Integer} id - Optional resource item id.
*/
async saveCustomFields(id) {
if (id) {
if (typeof this.fieldsMetadata[id] === 'undefined') {
throw new Error('There is no resource item with the given id.');
}
await this.fieldsMetadata[id].saveMeta();
} else {
const opers = [];
this.fieldsMetadata.forEach((metadata) => {
const oper = metadata.saveMeta();
opers.push(oper);
});
await Promise.all(opers);
}
}
/**
@@ -115,9 +144,11 @@ export default class ResourceCustomFieldRepository {
}
async load() {
await Promise.all([
this.loadResource(),
this.loadResourceCustomFields(),
]);
await this.loadResource();
await this.loadResourceCustomFields();
}
}
static forgeMetadataCollection() {
}
}