WIP Metadata class.

This commit is contained in:
Ahmed Bouhuolia
2019-09-08 02:41:46 +02:00
parent 70809cb05c
commit 9a8de9ca7d
29 changed files with 1707 additions and 98 deletions

View File

@@ -12,18 +12,34 @@ factory.define('user', 'users', async () => {
first_name: faker.name.firstName(),
last_name: faker.name.lastName(),
email: faker.internet.email(),
phone_number: faker.phone.phoneNumber(),
phone_number: faker.phone.phoneNumberFormat().replace('-', ''),
active: 1,
password: hashedPassword,
};
});
factory.define('account', 'accounts', async () => ({
name: faker.lorem.word(),
type: faker.lorem.word(),
description: faker.lorem.paragraph(),
factory.define('password_reset', 'password_resets', async () => {
const user = await faker.create('user');
return {
user_id: user.id,
token: faker.lorem.slug,
};
});
factory.define('account_type', 'account_types', async () => ({
name: faker.lorem.words(2),
}));
factory.define('account', 'accounts', async () => {
const accountType = await factory.create('account_type');
return {
name: faker.lorem.word(),
account_type_id: accountType.id,
description: faker.lorem.paragraph(),
};
});
factory.define('item_category', 'items_categories', () => ({
label: faker.name.firstName(),
description: faker.lorem.text(),
@@ -55,4 +71,16 @@ factory.define('item', 'items', async () => {
};
});
factory.define('setting', 'settings', async () => {
const user = await factory.create('user');
return {
key: faker.lorem.slug(),
user_id: user.id,
type: 'string',
value: faker.lorem.words(),
group: 'default',
};
});
export default factory;

View File

@@ -0,0 +1,9 @@
exports.up = (knex) => knex.schema.createTable('password_resets', (table) => {
table.increments();
table.string('user_id');
table.string('token');
table.timestamp('created_at');
});
exports.down = (knex) => knex.schema.dropTableIfExists('password_resets');

View File

@@ -1,6 +1,6 @@
exports.up = function(knex) {
return knex.schema.createTable('oauth_clients', table => {
return knex.schema.createTable('oauth_clients', (table) => {
table.increments();
table.integer('client_id').unsigned();
table.string('client_secret');

View File

@@ -1,8 +1,10 @@
exports.up = function(knex) {
return knex.schema.createTable('settings', table => {
exports.up = function (knex) {
return knex.schema.createTable('settings', (table) => {
table.increments();
table.integer('user_id').unsigned().references('id').inTable('users');
table.string('group');
table.string('type');
table.string('key');
table.string('value');
});

View File

@@ -3,7 +3,7 @@ exports.up = function (knex) {
return knex.schema.createTable('accounts', (table) => {
table.increments();
table.string('name');
table.string('type');
table.integer('account_type_id');
table.integer('parent_account_id');
table.string('code', 10);
table.text('description');

View File

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

View File

@@ -0,0 +1,63 @@
import express from 'express';
import { check, validationResult, oneOf } from 'express-validator';
import { difference } from 'lodash';
import asyncMiddleware from '../middleware/asyncMiddleware';
import Account from '@/models/Account';
// import AccountBalance from '@/models/AccountBalance';
export default {
router() {
const router = express.Router();
router.post('/',
this.openingBalnace.validation,
asyncMiddleware(this.openingBalnace.handler));
return router;
},
openingBalnace: {
validation: [
check('accounts').isArray({ min: 1 }),
check('accounts.*.id').exists().isInt(),
oneOf([
check('accounts.*.debit').isNumeric().toFloat(),
check('accounts.*.credit').isNumeric().toFloat(),
]),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { accounts } = req.body;
const accountsIds = accounts.map((account) => account.id);
const accountsCollection = await Account.query((query) => {
query.select(['id']);
query.whereIn('id', accountsIds);
}).fetchAll();
const accountsStoredIds = accountsCollection.map((account) => account.attributes.id);
const notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
const errorReasons = [];
if (notFoundAccountsIds.length > 0) {
const ids = notFoundAccountsIds.map((a) => parseInt(a, 10));
errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids });
}
if (errorReasons.length > 0) {
return res.boom.badData(null, { errors: errorReasons });
}
return res.status(200).send();
},
},
};

View File

@@ -4,7 +4,7 @@ import asyncMiddleware from '../middleware/asyncMiddleware';
import Account from '@/models/Account';
import AccountBalance from '@/models/AccountBalance';
import AccountType from '@/models/AccountType';
import JWTAuth from '@/http/middleware/jwtAuth';
// import JWTAuth from '@/http/middleware/jwtAuth';
export default {
/**
@@ -13,18 +13,22 @@ export default {
router() {
const router = express.Router();
router.use(JWTAuth);
// router.use(JWTAuth);
router.post('/',
this.newAccount.validation,
asyncMiddleware(this.newAccount.handler));
router.get('/:id',
this.getAccount.validation,
asyncMiddleware(this.getAccount.handler));
router.post('/:id',
this.editAccount.validation,
asyncMiddleware(this.editAccount.handler));
router.delete('/:id',
this.deleteAccount.validation,
asyncMiddleware(this.deleteAccount.handler));
// router.get('/:id',
// this.getAccount.validation,
// asyncMiddleware(this.getAccount.handler));
// router.delete('/:id',
// this.deleteAccount.validation,
// asyncMiddleware(this.deleteAccount.handler));
return router;
},
@@ -36,20 +40,22 @@ export default {
validation: [
check('name').isLength({ min: 3 }).trim().escape(),
check('code').isLength({ max: 10 }).trim().escape(),
check('type_id').isNumeric().toInt(),
check('account_type_id').isNumeric().toInt(),
check('description').trim().escape(),
],
async handler(req, res) {
const errors = validationResult(req);
const validationErrors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { name, code, description } = req.body;
const { type_id: typeId } = req.body;
const { account_type_id: typeId } = req.body;
const foundAccountCodePromise = Account.where('code', code).fetch();
const foundAccountCodePromise = code ? Account.where('code', code).fetch() : null;
const foundAccountTypePromise = AccountType.where('id', typeId).fetch();
const [foundAccountCode, foundAccountType] = await Promise.all([
@@ -57,7 +63,7 @@ export default {
foundAccountTypePromise,
]);
if (!foundAccountCode) {
if (!foundAccountCode && foundAccountCodePromise) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
});
@@ -68,11 +74,67 @@ export default {
});
}
const account = Account.forge({
name, code, type_id: typeId, description,
name, code, account_type_id: typeId, description,
});
await account.save();
return res.boom.success({ item: { ...account.attributes } });
return res.status(200).send({ item: { ...account.attributes } });
},
},
/**
* Edit the given account details.
*/
editAccount: {
validation: [
check('name').isLength({ min: 3 }).trim().escape(),
check('code').isLength({ max: 10 }).trim().escape(),
check('account_type_id').isNumeric().toInt(),
check('description').trim().escape(),
],
async handler(req, res) {
const { id } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const account = await Account.where('id', id).fetch();
if (!account) {
return res.boom.notFound();
}
const { name, code, description } = req.body;
const { account_type_id: typeId } = req.body;
const foundAccountCodePromise = (code && code !== account.attributes.code)
? Account.query({ where: { code }, whereNot: { id } }).fetch() : null;
const foundAccountTypePromise = (typeId !== account.attributes.account_type_id)
? AccountType.where('id', typeId).fetch() : null;
const [foundAccountCode, foundAccountType] = await Promise.all([
foundAccountCodePromise, foundAccountTypePromise,
]);
if (!foundAccountCode && foundAccountCodePromise) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
});
}
if (!foundAccountType && foundAccountTypePromise) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 110 }],
});
}
await account.save({
name, code, account_type_id: typeId, description,
});
return res.status(200).send();
},
},
@@ -105,7 +167,6 @@ export default {
if (!account) {
return res.boom.notFound();
}
await account.destroy();
await AccountBalance.where('account_id', id).destroy({ require: false });

View File

@@ -37,16 +37,15 @@ export default {
*/
login: {
validation: [
check('crediential').isEmail(),
check('password').isLength({ min: 5 }),
check('crediential').exists().isEmail(),
check('password').exists().isLength({ min: 5 }),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error',
...validationErrors,
code: 'validation_error', ...validationErrors,
});
}
const { crediential, password } = req.body;
@@ -81,17 +80,19 @@ export default {
*/
sendResetPassword: {
validation: [
check('email').isEmail(),
check('email').exists().isEmail(),
],
// eslint-disable-next-line consistent-return
async handler(req, res) {
const errors = validationResult(req);
const validationErrors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { email } = req.body;
const user = User.where('email').fetch();
const user = User.where('email', email).fetch();
if (!user) {
return res.status(422).send();
@@ -137,14 +138,21 @@ export default {
*/
resetPassword: {
validation: [
check('password').isLength({ min: 5 }),
check('reset_password'),
check('password').exists().isLength({ min: 5 }).custom((value, { req }) => {
if (value !== req.body.confirm_password) {
throw new Error("Passwords don't match");
} else {
return value;
}
}),
],
async handler(req, res) {
const errors = validationResult(req);
const validationErrors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'VALIDATION_ERROR', ...validationErrors,
});
}
const { token } = req.params;
const { password } = req.body;
@@ -155,11 +163,8 @@ export default {
}).fetch();
if (!tokenModel) {
return res.status(400).send({
error: {
type: 'token.invalid',
message: 'Password reset token is invalid or has expired',
},
return res.boom.badRequest(null, {
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
});
}
@@ -167,8 +172,8 @@ export default {
email: tokenModel.attributes.email,
});
if (!user) {
return res.status(400).send({
error: { message: 'An unexpected error occurred.' },
return res.boom.badRequest(null, {
errors: [{ type: 'USER_NOT_FOUND', code: 120 }],
});
}
const hashedPassword = await hashPassword(password);

View File

@@ -184,7 +184,10 @@ export default {
page: filter.page,
});
return res.status(200).send({ ...items.toJSON() });
return res.status(200).send({
items: items.toJSON(),
pagination: items.pagination,
});
},
},
};

View File

@@ -0,0 +1,241 @@
import express from 'express';
import { check, validationResult } from 'express-validator';
import User from '@/models/User';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.post('/',
this.newUser.validation,
asyncMiddleware(this.newUser.handler));
router.post('/:id',
this.editUser.validation,
asyncMiddleware(this.editUser.handler));
// router.get('/',
// this.listUsers.validation,
// asyncMiddleware(this.listUsers.handler));
// router.get('/:id',
// this.getUser.validation,
// asyncMiddleware(this.getUser.handler));
router.delete('/:id',
this.deleteUser.validation,
asyncMiddleware(this.deleteUser.handler));
return router;
},
/**
* Creates a new user.
*/
newUser: {
validation: [
check('first_name').exists(),
check('last_name').exists(),
check('email').exists().isEmail(),
check('phone_number').optional().isMobilePhone(),
check('password').isLength({ min: 4 }).exists().custom((value, { req }) => {
if (value !== req.body.confirm_password) {
throw new Error("Passwords don't match");
} else {
return value;
}
}),
check('status').exists().isBoolean().toBoolean(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { email, phone_number: phoneNumber } = req.body;
const foundUsers = await User.query((query) => {
query.where('email', email);
query.orWhere('phone_number', phoneNumber);
}).fetchAll();
const foundUserEmail = foundUsers.find((u) => u.attributes.email === email);
const foundUserPhone = foundUsers.find((u) => u.attributes.phone_number === phoneNumber);
const errorReasons = [];
if (foundUserEmail) {
errorReasons.push({ type: 'EMAIL_ALREADY_EXIST', code: 100 });
}
if (foundUserPhone) {
errorReasons.push({ type: 'PHONE_NUMBER_ALREADY_EXIST', code: 120 });
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
const user = User.forge({
first_name: req.body.first_name,
last_name: req.body.last_name,
email: req.body.email,
phone_number: req.body.phone_number,
active: req.body.status,
});
await user.save();
return res.status(200).send({ id: user.get('id') });
},
},
/**
* Edit details of the given user.
*/
editUser: {
validation: [
check('first_name').exists(),
check('last_name').exists(),
check('email').exists().isEmail(),
check('phone_number').optional().isMobilePhone(),
check('password').isLength({ min: 4 }).exists().custom((value, { req }) => {
if (value !== req.body.confirm_password) {
throw new Error("Passwords don't match");
} else {
return value;
}
}),
check('status').exists().isBoolean().toBoolean(),
],
async handler(req, res) {
const { id } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const user = await User.where('id', id).fetch();
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 foundUserEmail = foundUsers.find((u) => u.attribues.email === email);
const foundUserPhone = foundUsers.find((u) => u.attribues.phone_number === phoneNumber);
const errorReasons = [];
if (foundUserEmail) {
errorReasons.push({ type: 'EMAIL_ALREADY_EXIST', code: 100 });
}
if (foundUserPhone) {
errorReasons.push({ type: 'PHONE_NUMBER_ALREADY_EXIST', code: 120 });
}
if (errorReasons.length > 0) {
return res.badRequest(null, { errors: errorReasons });
}
await user.save({
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,
});
return res.status(200).send();
},
},
/**
* Soft deleting the given user.
*/
deleteUser: {
validation: [],
async handler(req, res) {
const { id } = req.params;
const user = await User.where('id', id).fetch();
if (!user) {
return res.boom.notFound(null, {
errors: [{ type: 'USER_NOT_FOUND', code: 100 }],
});
}
await user.destroy();
return res.status(200).send();
},
},
getUser: {
validation: [],
async handler(req, res) {
const { id } = req.params;
const user = await User.where('id', id).fetch();
if (!user) {
return res.boom.notFound();
}
return res.status(200).send({ item: user.toJSON() });
},
},
/**
* Retrieve the list of users.
*/
listUsers: {
validation: [],
handler(req, res) {
const filter = {
first_name: '',
last_name: '',
email: '',
phone_number: '',
page_size: 10,
page: 1,
...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,
});
return res.status(200).send({
items: users.toJSON(),
pagination: users.pagination,
});
},
},
};

View File

@@ -4,12 +4,14 @@ import Users from '@/http/controllers/Users';
import Items from '@/http/controllers/Items';
import ItemCategories from '@/http/controllers/ItemCategories';
import Accounts from '@/http/controllers/Accounts';
import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance';
export default (app) => {
// app.use('/api/oauth2', OAuth2.router());
app.use('/api/auth', Authentication.router());
app.use('/api/users', Users.router());
app.use('/api/accounts', Accounts.router());
app.use('/api/accountOpeningBalance', AccountOpeningBalance.router());
app.use('/api/items', Items.router());
app.use('/api/item_categories', ItemCategories.router());
};

View File

@@ -1,13 +1,18 @@
/* eslint-disable consistent-return */
import jwt from 'jsonwebtoken';
import User from '@/models/User';
import Auth from '@/models/Auth';
const authMiddleware = (req, res, next) => {
const token = req.headers['x-access-token'] || req.query.token;
const onError = () => res.status(401).send({
success: false,
message: 'unauthorized',
});
const onError = () => {
Auth.loggedOut();
res.status(401).send({
success: false,
message: 'unauthorized',
});
}
if (!token) {
return onError();
@@ -19,7 +24,9 @@ const authMiddleware = (req, res, next) => {
if (error) {
reject(error);
} else {
// eslint-disable-next-line no-underscore-dangle
req.user = await User.where('id', decoded._id).fetch();
Auth.setAuthenticatedUser(req.user);
if (!req.user) {
return onError();

View File

@@ -1,7 +1,6 @@
import bookshelf from './bookshelf';
const Account = bookshelf.Model.extend({
/**
* Table name
*/
@@ -11,6 +10,17 @@ const Account = bookshelf.Model.extend({
* Timestamp columns.
*/
hasTimestamps: ['created_at', 'updated_at'],
/**
* Account model may belongs to account type.
*/
type() {
return this.belongsTo('AccountType', 'account_type_id');
},
balances() {
return this.hasMany('AccountBalance', 'accounnt_id');
}
});
export default bookshelf.model('Account', Account);

View File

@@ -0,0 +1,23 @@
import bookshelf from './bookshelf';
const AccountBalance = bookshelf.Model.extend({
/**
* Table name
*/
tableName: 'account_balance',
/**
* Timestamp columns.
*/
hasTimestamps: false,
/**
*
*/
account() {
return this.belongsTo('Account', 'account_id');
},
});
export default bookshelf.model('AccountBalance', AccountBalance);

View File

@@ -0,0 +1,23 @@
import bookshelf from './bookshelf';
const AccountType = bookshelf.Model.extend({
/**
* Table name
*/
tableName: 'accounts',
/**
* Timestamp columns.
*/
hasTimestamps: false,
/**
* Account type may has many associated accounts.
*/
accounts() {
return this.hasMany('Account', 'account_type_id');
},
});
export default bookshelf.model('AccountType', AccountType);

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

@@ -0,0 +1,38 @@
export default class Auth {
/**
* Retrieve the authenticated user.
*/
static get user() {
return null;
}
/**
* Sets the authenticated user.
* @param {User} user
*/
static setAuthenticatedUser(user) {
this.user = user;
}
/**
* Retrieve the authenticated user ID.
*/
static userId() {
if (!this.user) {
return false;
}
return this.user.id;
}
/**
* Whether the user is logged or not.
*/
static isLogged() {
return !!this.user;
}
static loggedOut() {
this.user = null;
}
}

View File

@@ -0,0 +1,290 @@
import knex from '@/database/knex';
// import cache from 'memory-cache';
// Metadata
export default {
METADATA_GROUP: 'default',
KEY_COLUMN: 'key',
VALUE_COLUMN: 'value',
TYPE_COLUMN: 'type',
extraColumns: [],
metadata: [],
shouldReload: true,
extraMetadataQuery: () => {},
/**
* Set the value column key to query from.
* @param {String} name -
*/
setKeyColumnName(name) {
this.KEY_COLUMN = name;
},
/**
* Set the key column name to query from.
* @param {String} name -
*/
setValueColumnName(name) {
this.VALUE_COLUMN = name;
},
/**
* Set extra columns to be added to the rows.
* @param {Array} columns -
*/
setExtraColumns(columns) {
this.extraColumns = columns;
},
/**
* Retrieve the cache namespace.
*/
getCacheNamespace() {
const { metadataCacheNamespace: cacheName } = this;
return typeof cacheName === 'function'
? cacheName() : cacheName;
},
/**
* Metadata database query.
* @param {Object} query -
* @param {String} groupName -
*/
whereQuery(query, key) {
const groupName = this.METADATA_GROUP;
if (groupName) {
query.where('group', groupName);
}
if (key) {
if (Array.isArray(key)) {
query.whereIn('key', key);
} else {
query.where('key', key);
}
}
},
/**
* Loads the metadata from the storage.
* @param {String|Array} key -
* @param {Boolean} force -
*/
async load(force = false) {
if (this.shouldReload || force) {
const metadataCollection = await this.query((query) => {
this.whereQuery(query);
this.extraMetadataQuery(query);
}).fetchAll();
this.shouldReload = false;
this.metadata = [];
const metadataArray = this.mapMetadataCollection(metadataCollection);
metadataArray.forEach((metadata) => { this.metadata.push(metadata); });
}
},
/**
* Fetches all the metadata that associate with the current group.
*/
async allMeta(force = false) {
await this.load(force);
return this.metadata;
},
/**
* Find the given metadata key.
* @param {String} key -
* @return {object} - Metadata object.
*/
findMeta(key) {
return this.metadata.find((meta) => meta.key === key);
},
/**
* Fetch the metadata of the current group.
* @param {*} key -
*/
async getMeta(key, defaultValue, force = false) {
await this.load(force);
const metadata = this.findMeta(key);
return metadata ? metadata.value : defaultValue || false;
},
/**
* Markes the metadata to should be deleted.
* @param {String} key -
*/
async removeMeta(key) {
await this.load();
const metadata = this.findMeta(key);
if (metadata) {
metadata.markAsDeleted = true;
}
this.shouldReload = true;
},
/**
* Remove all meta data of the given group.
* @param {*} group
*/
removeAllMeta(group = 'default') {
this.metdata.map((meta) => ({
...(meta.group !== group) ? { markAsDeleted: true } : {},
...meta,
}));
this.shouldReload = true;
},
/**
* Set the meta data to the stack.
* @param {String} key -
* @param {String} value -
*/
async setMeta(key, value, payload) {
if (Array.isArray(key)) {
const metadata = key;
metadata.forEach((meta) => {
this.setMeta(meta.key, meta.value);
});
return;
}
await this.load();
const metadata = this.findMeta(key);
if (metadata) {
metadata.value = value;
metadata.markAsUpdated = true;
} else {
this.metadata.push({
value, key, ...payload, markAsInserted: true,
});
}
},
/**
* Saved the modified metadata.
*/
async saveMeta() {
const inserted = this.metadata.filter((m) => (m.markAsInserted === true));
const updated = this.metadata.filter((m) => (m.markAsUpdated === true));
const deleted = this.metadata.filter((m) => (m.markAsDeleted === true));
const metadataDeletedKeys = deleted.map((m) => m.key);
const metadataInserted = inserted.map((m) => this.mapMetadata(m, 'format'));
const metadataUpdated = updated.map((m) => this.mapMetadata(m, 'format'));
const batchUpdate = (collection) => knex.transaction((trx) => {
const queries = collection.map((tuple) => {
const query = knex(this.tableName);
this.whereQuery(query, tuple.key);
this.extraMetadataQuery(query);
return query.update(tuple).transacting(trx);
});
return Promise.all(queries).then(trx.commit).catch(trx.rollback);
});
await Promise.all([
knex.insert(metadataInserted).into(this.tableName),
batchUpdate(metadataUpdated),
metadataDeletedKeys.length > 0
? this.query('whereIn', this.KEY_COLUMN, metadataDeletedKeys).destroy({
require: true,
}) : null,
]);
this.shouldReload = true;
},
/**
* Purge all the cached metadata in the memory.
*/
purgeMetadata() {
this.metadata = [];
this.shouldReload = true;
},
/**
* Parses the metadata value.
* @param {String} value -
* @param {String} valueType -
*/
parseMetaValue(value, valueType) {
let parsedValue;
switch (valueType) {
case 'integer':
parsedValue = parseInt(value, 10);
break;
case 'float':
parsedValue = parseFloat(value);
break;
case 'boolean':
parsedValue = Boolean(value);
break;
case 'json':
parsedValue = JSON.parse(parsedValue);
break;
default:
parsedValue = value;
break;
}
return parsedValue;
},
/**
* Format the metadata before saving to the database.
* @param {String|Number|Boolean} value -
* @param {String} valueType -
* @return {String|Number|Boolean} -
*/
formatMetaValue(value, valueType) {
let parsedValue;
switch (valueType) {
case 'number':
parsedValue = `${value}`;
break;
case 'boolean':
parsedValue = value ? '1' : '0';
break;
case 'json':
parsedValue = JSON.stringify(parsedValue);
break;
default:
parsedValue = value;
break;
}
return parsedValue;
},
mapMetadata(attr, parseType = 'parse') {
return {
key: attr[this.KEY_COLUMN],
value: (parseType === 'parse')
? this.parseMetaValue(
attr[this.VALUE_COLUMN],
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
)
: this.formatMetaValue(
attr[this.VALUE_COLUMN],
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
),
...this.extraColumns.map((extraCol) => ({
[extraCol]: attr[extraCol] || null,
})),
};
},
/**
* Parse the metadata collection.
* @param {Array} collection -
*/
mapMetadataCollection(collection, parseType = 'parse') {
return collection.map((model) => this.mapMetadata(model.attributes, parseType));
},
};

View File

@@ -1,7 +1,8 @@
import bookshelf from './bookshelf';
import Metable from './Metable';
import Auth from './Auth';
const Setting = bookshelf.Model.extend({
/**
* Table name
*/
@@ -11,6 +12,23 @@ const Setting = bookshelf.Model.extend({
* Timestamp columns.
*/
hasTimestamps: false,
/**
* Extra metadata query to query with the current authenticate user.
* @param {Object} query
*/
extraMetadataQuery(query) {
if (Auth.isLogged()) {
query.where('user_id', Auth.userId());
}
},
}, {
/**
* Table name
*/
tableName: 'settings',
...Metable,
});
export default bookshelf.model('Setting', Setting);

View File

@@ -6,9 +6,7 @@ const hashPassword = (password) => new Promise((resolve) => {
});
});
const origin = (request) => {
return `${request.protocol}://${request.hostname}`;
};
const origin = (request) => `${request.protocol}://${request.hostname}`;
export {
hashPassword,