mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
WIP Metadata class.
This commit is contained in:
@@ -25,9 +25,11 @@
|
|||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-boom": "^3.0.0",
|
"express-boom": "^3.0.0",
|
||||||
"express-oauth-server": "^2.0.0",
|
"express-oauth-server": "^2.0.0",
|
||||||
"express-validator": "^6.1.1",
|
"express-validator": "^6.2.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"knex": "^0.19.2",
|
"knex": "^0.19.2",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"memory-cache": "^0.2.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"mustache": "^3.0.3",
|
"mustache": "^3.0.3",
|
||||||
"mysql2": "^1.6.5",
|
"mysql2": "^1.6.5",
|
||||||
@@ -57,6 +59,7 @@
|
|||||||
"mocha-webpack": "^2.0.0-beta.0",
|
"mocha-webpack": "^2.0.0-beta.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"nyc": "^14.1.1",
|
"nyc": "^14.1.1",
|
||||||
|
"sinon": "^7.4.2",
|
||||||
"webpack": "^4.0.0",
|
"webpack": "^4.0.0",
|
||||||
"webpack-cli": "^3.3.7",
|
"webpack-cli": "^3.3.7",
|
||||||
"webpack-node-externals": "^1.7.2"
|
"webpack-node-externals": "^1.7.2"
|
||||||
|
|||||||
@@ -12,18 +12,34 @@ factory.define('user', 'users', async () => {
|
|||||||
first_name: faker.name.firstName(),
|
first_name: faker.name.firstName(),
|
||||||
last_name: faker.name.lastName(),
|
last_name: faker.name.lastName(),
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
phone_number: faker.phone.phoneNumber(),
|
phone_number: faker.phone.phoneNumberFormat().replace('-', ''),
|
||||||
active: 1,
|
active: 1,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
factory.define('account', 'accounts', async () => ({
|
factory.define('password_reset', 'password_resets', async () => {
|
||||||
name: faker.lorem.word(),
|
const user = await faker.create('user');
|
||||||
type: faker.lorem.word(),
|
return {
|
||||||
description: faker.lorem.paragraph(),
|
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', () => ({
|
factory.define('item_category', 'items_categories', () => ({
|
||||||
label: faker.name.firstName(),
|
label: faker.name.firstName(),
|
||||||
description: faker.lorem.text(),
|
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;
|
export default factory;
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
exports.up = function(knex) {
|
exports.up = function(knex) {
|
||||||
return knex.schema.createTable('oauth_clients', table => {
|
return knex.schema.createTable('oauth_clients', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.integer('client_id').unsigned();
|
table.integer('client_id').unsigned();
|
||||||
table.string('client_secret');
|
table.string('client_secret');
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
|
||||||
exports.up = function(knex) {
|
exports.up = function (knex) {
|
||||||
return knex.schema.createTable('settings', table => {
|
return knex.schema.createTable('settings', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||||
|
table.string('group');
|
||||||
|
table.string('type');
|
||||||
table.string('key');
|
table.string('key');
|
||||||
table.string('value');
|
table.string('value');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ exports.up = function (knex) {
|
|||||||
return knex.schema.createTable('accounts', (table) => {
|
return knex.schema.createTable('accounts', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.string('name');
|
table.string('name');
|
||||||
table.string('type');
|
table.integer('account_type_id');
|
||||||
table.integer('parent_account_id');
|
table.integer('parent_account_id');
|
||||||
table.string('code', 10);
|
table.string('code', 10);
|
||||||
table.text('description');
|
table.text('description');
|
||||||
|
|||||||
@@ -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');
|
||||||
63
server/src/http/controllers/AccountOpeningBalance.js
Normal file
63
server/src/http/controllers/AccountOpeningBalance.js
Normal 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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ import asyncMiddleware from '../middleware/asyncMiddleware';
|
|||||||
import Account from '@/models/Account';
|
import Account from '@/models/Account';
|
||||||
import AccountBalance from '@/models/AccountBalance';
|
import AccountBalance from '@/models/AccountBalance';
|
||||||
import AccountType from '@/models/AccountType';
|
import AccountType from '@/models/AccountType';
|
||||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
// import JWTAuth from '@/http/middleware/jwtAuth';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -13,18 +13,22 @@ export default {
|
|||||||
router() {
|
router() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use(JWTAuth);
|
// router.use(JWTAuth);
|
||||||
router.post('/',
|
router.post('/',
|
||||||
this.newAccount.validation,
|
this.newAccount.validation,
|
||||||
asyncMiddleware(this.newAccount.handler));
|
asyncMiddleware(this.newAccount.handler));
|
||||||
|
|
||||||
router.get('/:id',
|
router.post('/:id',
|
||||||
this.getAccount.validation,
|
this.editAccount.validation,
|
||||||
asyncMiddleware(this.getAccount.handler));
|
asyncMiddleware(this.editAccount.handler));
|
||||||
|
|
||||||
router.delete('/:id',
|
// router.get('/:id',
|
||||||
this.deleteAccount.validation,
|
// this.getAccount.validation,
|
||||||
asyncMiddleware(this.deleteAccount.handler));
|
// asyncMiddleware(this.getAccount.handler));
|
||||||
|
|
||||||
|
// router.delete('/:id',
|
||||||
|
// this.deleteAccount.validation,
|
||||||
|
// asyncMiddleware(this.deleteAccount.handler));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
},
|
},
|
||||||
@@ -36,20 +40,22 @@ export default {
|
|||||||
validation: [
|
validation: [
|
||||||
check('name').isLength({ min: 3 }).trim().escape(),
|
check('name').isLength({ min: 3 }).trim().escape(),
|
||||||
check('code').isLength({ max: 10 }).trim().escape(),
|
check('code').isLength({ max: 10 }).trim().escape(),
|
||||||
check('type_id').isNumeric().toInt(),
|
check('account_type_id').isNumeric().toInt(),
|
||||||
check('description').trim().escape(),
|
check('description').trim().escape(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const errors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
if (!errors.isEmpty()) {
|
if (!validationErrors.isEmpty()) {
|
||||||
return res.status(422).json({ errors: errors.array() });
|
return res.boom.badData(null, {
|
||||||
|
code: 'validation_error', ...validationErrors,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, code, description } = req.body;
|
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 foundAccountTypePromise = AccountType.where('id', typeId).fetch();
|
||||||
|
|
||||||
const [foundAccountCode, foundAccountType] = await Promise.all([
|
const [foundAccountCode, foundAccountType] = await Promise.all([
|
||||||
@@ -57,7 +63,7 @@ export default {
|
|||||||
foundAccountTypePromise,
|
foundAccountTypePromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!foundAccountCode) {
|
if (!foundAccountCode && foundAccountCodePromise) {
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
|
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
|
||||||
});
|
});
|
||||||
@@ -68,11 +74,67 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const account = Account.forge({
|
const account = Account.forge({
|
||||||
name, code, type_id: typeId, description,
|
name, code, account_type_id: typeId, description,
|
||||||
});
|
});
|
||||||
|
|
||||||
await account.save();
|
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) {
|
if (!account) {
|
||||||
return res.boom.notFound();
|
return res.boom.notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
await account.destroy();
|
await account.destroy();
|
||||||
await AccountBalance.where('account_id', id).destroy({ require: false });
|
await AccountBalance.where('account_id', id).destroy({ require: false });
|
||||||
|
|
||||||
|
|||||||
@@ -37,16 +37,15 @@ export default {
|
|||||||
*/
|
*/
|
||||||
login: {
|
login: {
|
||||||
validation: [
|
validation: [
|
||||||
check('crediential').isEmail(),
|
check('crediential').exists().isEmail(),
|
||||||
check('password').isLength({ min: 5 }),
|
check('password').exists().isLength({ min: 5 }),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const validationErrors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
if (!validationErrors.isEmpty()) {
|
if (!validationErrors.isEmpty()) {
|
||||||
return res.boom.badData(null, {
|
return res.boom.badData(null, {
|
||||||
code: 'validation_error',
|
code: 'validation_error', ...validationErrors,
|
||||||
...validationErrors,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { crediential, password } = req.body;
|
const { crediential, password } = req.body;
|
||||||
@@ -81,17 +80,19 @@ export default {
|
|||||||
*/
|
*/
|
||||||
sendResetPassword: {
|
sendResetPassword: {
|
||||||
validation: [
|
validation: [
|
||||||
check('email').isEmail(),
|
check('email').exists().isEmail(),
|
||||||
],
|
],
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const errors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
if (!errors.isEmpty()) {
|
if (!validationErrors.isEmpty()) {
|
||||||
return res.status(422).json({ errors: errors.array() });
|
return res.boom.badData(null, {
|
||||||
|
code: 'validation_error', ...validationErrors,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const { email } = req.body;
|
const { email } = req.body;
|
||||||
const user = User.where('email').fetch();
|
const user = User.where('email', email).fetch();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(422).send();
|
return res.status(422).send();
|
||||||
@@ -137,14 +138,21 @@ export default {
|
|||||||
*/
|
*/
|
||||||
resetPassword: {
|
resetPassword: {
|
||||||
validation: [
|
validation: [
|
||||||
check('password').isLength({ min: 5 }),
|
check('password').exists().isLength({ min: 5 }).custom((value, { req }) => {
|
||||||
check('reset_password'),
|
if (value !== req.body.confirm_password) {
|
||||||
|
throw new Error("Passwords don't match");
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const errors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
if (!errors.isEmpty()) {
|
if (!validationErrors.isEmpty()) {
|
||||||
return res.status(422).json({ errors: errors.array() });
|
return res.boom.badData(null, {
|
||||||
|
code: 'VALIDATION_ERROR', ...validationErrors,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const { token } = req.params;
|
const { token } = req.params;
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
@@ -155,11 +163,8 @@ export default {
|
|||||||
}).fetch();
|
}).fetch();
|
||||||
|
|
||||||
if (!tokenModel) {
|
if (!tokenModel) {
|
||||||
return res.status(400).send({
|
return res.boom.badRequest(null, {
|
||||||
error: {
|
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
|
||||||
type: 'token.invalid',
|
|
||||||
message: 'Password reset token is invalid or has expired',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +172,8 @@ export default {
|
|||||||
email: tokenModel.attributes.email,
|
email: tokenModel.attributes.email,
|
||||||
});
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(400).send({
|
return res.boom.badRequest(null, {
|
||||||
error: { message: 'An unexpected error occurred.' },
|
errors: [{ type: 'USER_NOT_FOUND', code: 120 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const hashedPassword = await hashPassword(password);
|
const hashedPassword = await hashPassword(password);
|
||||||
|
|||||||
@@ -184,7 +184,10 @@ export default {
|
|||||||
page: filter.page,
|
page: filter.page,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send({ ...items.toJSON() });
|
return res.status(200).send({
|
||||||
|
items: items.toJSON(),
|
||||||
|
pagination: items.pagination,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import Users from '@/http/controllers/Users';
|
|||||||
import Items from '@/http/controllers/Items';
|
import Items from '@/http/controllers/Items';
|
||||||
import ItemCategories from '@/http/controllers/ItemCategories';
|
import ItemCategories from '@/http/controllers/ItemCategories';
|
||||||
import Accounts from '@/http/controllers/Accounts';
|
import Accounts from '@/http/controllers/Accounts';
|
||||||
|
import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance';
|
||||||
|
|
||||||
export default (app) => {
|
export default (app) => {
|
||||||
// app.use('/api/oauth2', OAuth2.router());
|
// app.use('/api/oauth2', OAuth2.router());
|
||||||
app.use('/api/auth', Authentication.router());
|
app.use('/api/auth', Authentication.router());
|
||||||
app.use('/api/users', Users.router());
|
app.use('/api/users', Users.router());
|
||||||
app.use('/api/accounts', Accounts.router());
|
app.use('/api/accounts', Accounts.router());
|
||||||
|
app.use('/api/accountOpeningBalance', AccountOpeningBalance.router());
|
||||||
app.use('/api/items', Items.router());
|
app.use('/api/items', Items.router());
|
||||||
app.use('/api/item_categories', ItemCategories.router());
|
app.use('/api/item_categories', ItemCategories.router());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
/* eslint-disable consistent-return */
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import User from '@/models/User';
|
import User from '@/models/User';
|
||||||
|
import Auth from '@/models/Auth';
|
||||||
|
|
||||||
const authMiddleware = (req, res, next) => {
|
const authMiddleware = (req, res, next) => {
|
||||||
const token = req.headers['x-access-token'] || req.query.token;
|
const token = req.headers['x-access-token'] || req.query.token;
|
||||||
|
|
||||||
const onError = () => res.status(401).send({
|
const onError = () => {
|
||||||
success: false,
|
Auth.loggedOut();
|
||||||
message: 'unauthorized',
|
res.status(401).send({
|
||||||
});
|
success: false,
|
||||||
|
message: 'unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return onError();
|
return onError();
|
||||||
@@ -19,7 +24,9 @@ const authMiddleware = (req, res, next) => {
|
|||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
req.user = await User.where('id', decoded._id).fetch();
|
req.user = await User.where('id', decoded._id).fetch();
|
||||||
|
Auth.setAuthenticatedUser(req.user);
|
||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return onError();
|
return onError();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import bookshelf from './bookshelf';
|
import bookshelf from './bookshelf';
|
||||||
|
|
||||||
const Account = bookshelf.Model.extend({
|
const Account = bookshelf.Model.extend({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
@@ -11,6 +10,17 @@ const Account = bookshelf.Model.extend({
|
|||||||
* Timestamp columns.
|
* Timestamp columns.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: ['created_at', 'updated_at'],
|
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);
|
export default bookshelf.model('Account', Account);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
38
server/src/models/Auth.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
290
server/src/models/Metable.js
Normal file
290
server/src/models/Metable.js
Normal 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));
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import bookshelf from './bookshelf';
|
import bookshelf from './bookshelf';
|
||||||
|
import Metable from './Metable';
|
||||||
|
import Auth from './Auth';
|
||||||
|
|
||||||
const Setting = bookshelf.Model.extend({
|
const Setting = bookshelf.Model.extend({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
@@ -11,6 +12,23 @@ const Setting = bookshelf.Model.extend({
|
|||||||
* Timestamp columns.
|
* Timestamp columns.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: false,
|
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);
|
export default bookshelf.model('Setting', Setting);
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ const hashPassword = (password) => new Promise((resolve) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const origin = (request) => {
|
const origin = (request) => `${request.protocol}://${request.hostname}`;
|
||||||
return `${request.protocol}://${request.hostname}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
hashPassword,
|
hashPassword,
|
||||||
|
|||||||
16
server/tests/models/Account.test.js
Normal file
16
server/tests/models/Account.test.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { create, expect } from '~/testInit';
|
||||||
|
import Account from '@/models/Account';
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import AccountType from '@/models/AccountType';
|
||||||
|
|
||||||
|
describe('Model: Account', () => {
|
||||||
|
it('Should account model belongs to the associated account type model.', async () => {
|
||||||
|
const accountType = await create('account_type');
|
||||||
|
const account = await create('account', { account_type_id: accountType.id });
|
||||||
|
|
||||||
|
const accountModel = await Account.where('id', account.id).fetch();
|
||||||
|
const accountTypeModel = await accountModel.type().fetch();
|
||||||
|
|
||||||
|
expect(accountTypeModel.attributes.id).equals(account.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
16
server/tests/models/AccountType.test.js
Normal file
16
server/tests/models/AccountType.test.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { create, expect } from '~/testInit';
|
||||||
|
import '@/models/Account';
|
||||||
|
import AccountType from '@/models/AccountType';
|
||||||
|
|
||||||
|
describe.only('Model: AccountType', () => {
|
||||||
|
it('Shoud account type model has many associated accounts.', async () => {
|
||||||
|
const accountType = await create('account_type');
|
||||||
|
await create('account', { account_type_id: accountType.id });
|
||||||
|
await create('account', { account_type_id: accountType.id });
|
||||||
|
|
||||||
|
const accountTypeModel = await AccountType.where('id', accountType.id).fetch();
|
||||||
|
const typeAccounts = await accountTypeModel.accounts().fetch();
|
||||||
|
|
||||||
|
expect(typeAccounts.length).equals(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
197
server/tests/models/Setting.test.js
Normal file
197
server/tests/models/Setting.test.js
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import sinon from 'sinon';
|
||||||
|
import { create, expect } from '~/testInit';
|
||||||
|
import Setting from '@/models/Setting';
|
||||||
|
import knex from '../../src/database/knex';
|
||||||
|
|
||||||
|
describe('Model: Setting', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
Setting.purgeMetadata();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Setting.AllMeta()', async () => {
|
||||||
|
it('Should fetch all metadata from storage in the first call.', async () => {
|
||||||
|
await create('setting');
|
||||||
|
const querySpy = sinon.spy(Setting, 'query');
|
||||||
|
|
||||||
|
const metadata = await Setting.allMeta();
|
||||||
|
|
||||||
|
expect(querySpy.calledOnce).equals(true);
|
||||||
|
expect(metadata).to.have.lengthOf(1);
|
||||||
|
|
||||||
|
querySpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should get all meta data from stored cache in the second call.', async () => {
|
||||||
|
await create('setting');
|
||||||
|
const querySpy = sinon.spy(Setting, 'query');
|
||||||
|
|
||||||
|
await Setting.allMeta();
|
||||||
|
await Setting.allMeta();
|
||||||
|
|
||||||
|
expect(querySpy.calledOnce).equals(true);
|
||||||
|
expect(Setting.metadata).to.have.lengthOf(1);
|
||||||
|
|
||||||
|
querySpy.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Setting.getMeta()', () => {
|
||||||
|
it('Should fetch metadata of the given key from storage.', async () => {
|
||||||
|
const setting = await create('setting');
|
||||||
|
const metadata = await Setting.getMeta(setting.key);
|
||||||
|
|
||||||
|
expect(metadata).equals(setting.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should retrieve the default value if the metadata key was not found.', async () => {
|
||||||
|
const metadata = await Setting.getMeta('setting', 'default');
|
||||||
|
expect(metadata).equals('default');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should get the same metadata key from cache in the second call.', async () => {
|
||||||
|
const setting = await create('setting');
|
||||||
|
await create('setting');
|
||||||
|
|
||||||
|
const querySpy = sinon.spy(Setting, 'query');
|
||||||
|
|
||||||
|
await Setting.getMeta(setting.key);
|
||||||
|
expect(querySpy.calledOnce).equals(true);
|
||||||
|
|
||||||
|
await Setting.getMeta(setting.key);
|
||||||
|
expect(querySpy.calledOnce).equals(true);
|
||||||
|
|
||||||
|
querySpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should get the different metadata key from storage.', async () => {
|
||||||
|
const setting = await create('setting');
|
||||||
|
const settingAnother = await create('setting');
|
||||||
|
|
||||||
|
const querySpy = sinon.spy(Setting, 'query');
|
||||||
|
|
||||||
|
await Setting.getMeta(setting.key);
|
||||||
|
expect(querySpy.calledOnce).equals(true);
|
||||||
|
|
||||||
|
await Setting.getMeta(settingAnother.key);
|
||||||
|
expect(querySpy.calledOnce).equals(true);
|
||||||
|
|
||||||
|
querySpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should hard fetching the metadata from the storage when passing `force` parameter.', async () => {
|
||||||
|
const setting = await create('setting');
|
||||||
|
await create('setting');
|
||||||
|
|
||||||
|
const querySpy = sinon.spy(Setting, 'query');
|
||||||
|
|
||||||
|
await Setting.allMeta();
|
||||||
|
expect(querySpy.calledOnce).equals(true);
|
||||||
|
expect(Setting.metadata).to.have.lengthOf(2);
|
||||||
|
|
||||||
|
await Setting.getMeta(setting.key, null, true);
|
||||||
|
expect(querySpy.calledTwice).equals(true);
|
||||||
|
expect(Setting.metadata).to.have.lengthOf(2);
|
||||||
|
|
||||||
|
querySpy.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Setting.setMeta()', () => {
|
||||||
|
it('Should mark the given metadata as updated in the stack.', async () => {
|
||||||
|
const setting = await create('setting');
|
||||||
|
await Setting.setMeta(setting.key, 'Ahmed');
|
||||||
|
|
||||||
|
const foundMeta = Setting.metadata.find((metadata) => (
|
||||||
|
metadata.key === setting.key && metadata.markAsUpdated === true
|
||||||
|
&& metadata.value === 'Ahmed'
|
||||||
|
));
|
||||||
|
expect(!!foundMeta).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should mark the set metadata as inserted metadata in the stack.', async () => {
|
||||||
|
await create('setting');
|
||||||
|
await Setting.setMeta('key', 'value');
|
||||||
|
|
||||||
|
const foundMeta = Setting.metadata.find((metadata) => (
|
||||||
|
metadata.key === 'key' && metadata.markAsInserted === true
|
||||||
|
&& metadata.value === 'value'
|
||||||
|
));
|
||||||
|
expect(!!foundMeta).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should fetch the metadata from the storage in case the metadata was exist.', async () => {
|
||||||
|
const setting = await create('setting');
|
||||||
|
const querySpy = sinon.spy(Setting, 'query');
|
||||||
|
|
||||||
|
await Setting.setMeta(setting.key, 'value');
|
||||||
|
expect(querySpy.calledOnce).equals(true);
|
||||||
|
|
||||||
|
await Setting.setMeta(setting.key, 'updated-value');
|
||||||
|
expect(querySpy.calledOnce).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should mark the updated bluk metadata as updated in the stock.', async () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should mark the inserted bluk metadata as inserted in the stock.', async () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Setting.removeMeta()', () => {
|
||||||
|
it('Should mark the given metadata as deleted', async () => {
|
||||||
|
const setting = await create('setting');
|
||||||
|
await Setting.removeMeta(setting.key);
|
||||||
|
|
||||||
|
const foundMeta = Setting.metadata.find((metadata) => (
|
||||||
|
metadata.key === setting.key && metadata.markAsDeleted === true
|
||||||
|
));
|
||||||
|
expect(!!foundMeta).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not query the storage when found cached the metadata.', async () => {
|
||||||
|
const setting = await create('setting');
|
||||||
|
await Setting.allMeta();
|
||||||
|
|
||||||
|
const querySpy = sinon.spy(Setting, 'query');
|
||||||
|
|
||||||
|
await Setting.removeMeta(setting.key);
|
||||||
|
expect(querySpy.calledOnce).equals(false);
|
||||||
|
|
||||||
|
querySpy.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Setting.saveMeta()', () => {
|
||||||
|
it('Should insert the metadata that set to the stock.', async () => {
|
||||||
|
await Setting.setMeta('key', 'value');
|
||||||
|
await Setting.saveMeta();
|
||||||
|
|
||||||
|
const storedMetadata = await knex('settings');
|
||||||
|
expect(storedMetadata).to.have.lengthOf(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should update the metadata that updated in the stock.', async () => {
|
||||||
|
const setting = await create('setting');
|
||||||
|
|
||||||
|
await Setting.setMeta(setting.key, 'value');
|
||||||
|
await Setting.saveMeta();
|
||||||
|
|
||||||
|
const storedMetadata = await knex('settings');
|
||||||
|
|
||||||
|
expect(storedMetadata).to.have.lengthOf(1);
|
||||||
|
expect(storedMetadata[0].value).equals('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should delete the metadata that removed from the stock.', async () => {
|
||||||
|
const setting = await create('setting');
|
||||||
|
|
||||||
|
await Setting.removeMeta(setting.key);
|
||||||
|
await Setting.saveMeta();
|
||||||
|
|
||||||
|
const storedMetadata = await knex('settings');
|
||||||
|
expect(storedMetadata).to.have.lengthOf(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
44
server/tests/routes/accountOpeningBalance.test.js
Normal file
44
server/tests/routes/accountOpeningBalance.test.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { request, expect } from '~/testInit';
|
||||||
|
|
||||||
|
describe.only('routes: `/accountOpeningBalance`', () => {
|
||||||
|
describe('POST `/accountOpeningBalance`', () => {
|
||||||
|
it('Should `accounts` be array type.', async () => {
|
||||||
|
const res = await request().post('/api/accountOpeningBalance').send({
|
||||||
|
accounts: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('VALIDATION_ERROR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `accounts.*.id` be integer', async () => {
|
||||||
|
const res = await request().post('/api/accountOpeningBalance').send({
|
||||||
|
accounts: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('VALIDATION_ERROR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `accounts.*.debit` be numeric.', async () => {
|
||||||
|
const res = await request().post('/api/accountOpeningBalance').send({
|
||||||
|
accounts: [{ id: 'id' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.only('Should `accounts.*.id` be exist in the storage.', async () => {
|
||||||
|
const res = await request().post('/api/accountOpeningBalance').send({
|
||||||
|
accounts: [
|
||||||
|
{ id: 100, credit: 100, debit: 100 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.errors).include.something.that.deep.equals({
|
||||||
|
type: 'NOT_FOUND_ACCOUNT', code: 100, ids: [100],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
138
server/tests/routes/accounts.test.js
Normal file
138
server/tests/routes/accounts.test.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { request, expect, create } from '~/testInit';
|
||||||
|
import knex from '@/database/knex';
|
||||||
|
|
||||||
|
describe('routes: /accounts/', () => {
|
||||||
|
describe('POST `/accounts`', () => {
|
||||||
|
it('Should `name` be required.', async () => {
|
||||||
|
const res = await request().post('/api/accounts').send();
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('validation_error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `account_type_id` be required.', async () => {
|
||||||
|
const res = await request().post('/api/accounts').send();
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('validation_error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should max length of `code` be limited.', async () => {
|
||||||
|
const res = await request().post('/api/accounts').send();
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('validation_error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should response type not found in case `account_type_id` was not exist.', async () => {
|
||||||
|
const res = await request().post('/api/accounts').send();
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should account code be unique in the storage.', async () => {
|
||||||
|
const account = await create('account');
|
||||||
|
const res = await request().post('/api/accounts').send({
|
||||||
|
...account,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(400);
|
||||||
|
expect(res.body.errors).include.something.that.deep.equals({
|
||||||
|
type: 'PARENT_CATEGORY_NOT_FOUND', code: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should response success with correct data form.', async () => {
|
||||||
|
const account = await create('account');
|
||||||
|
const res = await request().post('/api/accounts').send({
|
||||||
|
name: 'Name',
|
||||||
|
description: 'description here',
|
||||||
|
account_type_id: account.account_type_id,
|
||||||
|
parent_account_id: account.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should store account data in the storage.', async () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST `/accounts/:id`', () => {
|
||||||
|
it('Should `name` be required.', async () => {
|
||||||
|
const account = await create('account');
|
||||||
|
const res = await request().post(`/api/accounts/${account.id}`).send();
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('validation_error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `account_type_id` be required.', async () => {
|
||||||
|
const account = await create('account');
|
||||||
|
const res = await request().post(`/api/accounts/${account.id}`).send();
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('validation_error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should max length of `code` be limited.', async () => {
|
||||||
|
const account = await create('account');
|
||||||
|
const res = await request().post(`/api/accounts/${account.id}`).send();
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('validation_error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should response type not found in case `account_type_id` was not exist.', async () => {
|
||||||
|
const res = await request().post('/api/accounts').send();
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should account code be unique in the storage.', async () => {
|
||||||
|
const account = await create('account', { code: 'ABCD' });
|
||||||
|
const res = await request().post(`/api/accounts/${account.id}`).send({
|
||||||
|
// code: ',
|
||||||
|
...account,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(400);
|
||||||
|
expect(res.body.errors).include.something.that.deep.equals({
|
||||||
|
type: 'NOT_UNIQUE_CODE', code: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should response success with correct data form.', async () => {
|
||||||
|
const account = await create('account');
|
||||||
|
const res = await request().post('/api/accounts').send({
|
||||||
|
name: 'Name',
|
||||||
|
description: 'description here',
|
||||||
|
account_type_id: account.account_type_id,
|
||||||
|
parent_account_id: account.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET: `/accounts`', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE: `/accounts`', () => {
|
||||||
|
it('Should response not found in case account was not exist.', async () => {
|
||||||
|
const res = await request().delete('/api/accounts/10').send();
|
||||||
|
|
||||||
|
expect(res.status).equals(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should delete the give account from the storage.', async () => {
|
||||||
|
const account = await create('account');
|
||||||
|
await request().delete(`/api/accounts/${account.id}`);
|
||||||
|
|
||||||
|
const foundAccounts = await knex('accounts').where('id', account.id);
|
||||||
|
expect(foundAccounts).to.have.lengthOf(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { request, expect, create } from '~/testInit';
|
import { request, expect, create } from '~/testInit';
|
||||||
import { hashPassword } from '@/utils';
|
import { hashPassword } from '@/utils';
|
||||||
|
import knex from '@/database/knex';
|
||||||
|
|
||||||
describe('routes: /auth/', () => {
|
describe('routes: /auth/', () => {
|
||||||
describe('POST `/api/auth/login`', () => {
|
describe('POST `/api/auth/login`', () => {
|
||||||
@@ -110,61 +111,136 @@ describe('routes: /auth/', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// describe('POST: `auth/send_reset_password`', () => {
|
describe('POST: `/auth/send_reset_password`', () => {
|
||||||
|
it('Should `email` be required.', async () => {
|
||||||
|
const res = await request().post('/api/auth/send_reset_password').send();
|
||||||
|
|
||||||
// it('Should `email` be required.', () => {
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('validation_error');
|
||||||
|
});
|
||||||
|
|
||||||
// });
|
it('Should response unproccessable if the email address was invalid.', async () => {
|
||||||
|
const res = await request().post('/api/auth/send_reset_password').send({
|
||||||
|
email: 'invalid_email',
|
||||||
|
});
|
||||||
|
|
||||||
// it('Should response unproccessable if the email address was invalid.', () => {
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('validation_error');
|
||||||
|
});
|
||||||
|
|
||||||
// });
|
it('Should response unproccessable if the email address was not exist.', async () => {
|
||||||
|
const res = await request().post('/api/auth/send_reset_password').send({
|
||||||
|
email: 'admin@admin.com',
|
||||||
|
});
|
||||||
|
|
||||||
// it('Should response unproccessable if the email address was not exist.', () => {
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.errors).include.something.that.deep.equals({
|
||||||
|
type: 'EMAIL_NOT_FOUND', code: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// });
|
it('Should delete all already tokens that associate to the given email.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const token = '123123';
|
||||||
|
|
||||||
// it('Should delete all already tokens that associate to the given email.', () => {
|
await knex('password_resets').insert({ email: user.email, token });
|
||||||
|
await request().post('/api/auth/send_reset_password').send({
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
|
||||||
// });
|
const oldPasswordToken = await knex('password_resets').where('token', token);
|
||||||
|
|
||||||
// it('Should store new token associate with the given email.', () => {
|
expect(oldPasswordToken).to.have.lengthOf(0);
|
||||||
|
});
|
||||||
|
|
||||||
// });
|
it('Should store new token associate with the given email.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
await request().post('/api/auth/send_reset_password').send({
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
|
||||||
// it('Should response success if the email was exist.', () => {
|
const token = await knex('password_resets').where('email', user.email);
|
||||||
|
|
||||||
// });
|
expect(token).to.have.lengthOf(1);
|
||||||
|
});
|
||||||
|
|
||||||
// it('Should token be stored to the table after success request.', () => {
|
it('Should response success if the email was exist.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const res = await request().post('/api/auth/send_reset_password').send({
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
|
||||||
// });
|
expect(res.status).equals(200);
|
||||||
// });
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// describe('POST: `/auth/reset/:token`', () => {
|
describe('POST: `/auth/reset/:token`', () => {
|
||||||
|
// it('Should response forbidden if the token was invalid.', () => {
|
||||||
|
|
||||||
// it('Should response forbidden if the token was invalid.', () => {
|
// });
|
||||||
|
|
||||||
// });
|
it('Should response forbidden if the token was expired.', () => {
|
||||||
|
|
||||||
// it('Should response forbidden if the token was expired.', () => {
|
});
|
||||||
|
|
||||||
// });
|
it('Should `password` be required.', async () => {
|
||||||
|
const passwordReset = await create('password_reset');
|
||||||
|
const res = await request().post(`/api/reset/${passwordReset.token}`).send();
|
||||||
|
|
||||||
// it('Should password be required.', () => {
|
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('password');
|
||||||
|
});
|
||||||
|
|
||||||
// it('Should password and confirm_password be equal.', () => {
|
it('Should password and confirm_password be equal.', async () => {
|
||||||
|
const passwordReset = await create('password_reset');
|
||||||
|
const res = await request().post(`/api/reset/${passwordReset.token}`).send({
|
||||||
|
password: '123123',
|
||||||
|
});
|
||||||
|
|
||||||
// });
|
expect(res.status).equals(422);
|
||||||
|
expect(res.body.code).equals('VALIDATION_ERROR');
|
||||||
|
|
||||||
// it('Should token be deleted after success response.', () => {
|
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||||
|
expect(paramsErrors).to.include('password');
|
||||||
|
});
|
||||||
|
|
||||||
// });
|
it('Should response success with correct data form.', async () => {
|
||||||
|
const passwordReset = await create('password_reset');
|
||||||
|
const res = await request().post(`/api/reset/${passwordReset.token}`).send({
|
||||||
|
password: '123123',
|
||||||
|
confirm_password: '123123',
|
||||||
|
});
|
||||||
|
|
||||||
// it('Should password be updated after success response.', () => {
|
expect(res.status).equals(200);
|
||||||
|
});
|
||||||
|
|
||||||
// })
|
it('Should token be deleted after success response.', async () => {
|
||||||
// });
|
const passwordReset = await create('password_reset');
|
||||||
|
await request().post(`/api/reset/${passwordReset.token}`).send({
|
||||||
|
password: '123123',
|
||||||
|
confirm_password: '123123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const foundTokens = await knex('password_resets').where('email', passwordReset.email);
|
||||||
|
expect(foundTokens).to.have.lengthOf(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should password be updated after success response.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const passwordReset = await create('password_reset', { user_id: user.id });
|
||||||
|
|
||||||
|
await request().post(`/api/reset/${passwordReset.token}`).send({
|
||||||
|
password: '123123',
|
||||||
|
confirm_password: '123123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const foundUser = await knex('users').where('id', user.id);
|
||||||
|
|
||||||
|
expect(foundUser.id).equals(user.id);
|
||||||
|
expect(foundUser.password).not.equals(user.password);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
describe('routes: `/roles/`', () => {
|
||||||
|
describe('POST: `/roles/`', () => {
|
||||||
|
it('Should name be required.', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `permissions` be ', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should response success with correct data format.', async () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should save the given role details in the storage.', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE: `/roles/:id`', () => {
|
||||||
|
it('Should not delete the predefined role.', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import knex from '@/database/knex';
|
||||||
|
import {
|
||||||
|
request,
|
||||||
|
expect,
|
||||||
|
create,
|
||||||
|
make,
|
||||||
|
} from '~/testInit';
|
||||||
|
|
||||||
describe('routes: `/routes`', () => {
|
describe('routes: `/routes`', () => {
|
||||||
describe('POST: `/routes`', () => {
|
describe('POST: `/routes`', () => {
|
||||||
@@ -5,16 +12,266 @@ describe('routes: `/routes`', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should `email` be required.', () => {
|
it('Should `first_name` be required.', async () => {
|
||||||
|
const res = await request().post('/api/users');
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const foundFirstNameParam = res.body.errors.find((error) => error.param === 'first_name');
|
||||||
|
expect(!!foundFirstNameParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `last_name` be required.', async () => {
|
||||||
|
const res = await request().post('/api/users');
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const foundFirstNameParam = res.body.errors.find((error) => error.param === 'last_name');
|
||||||
|
expect(!!foundFirstNameParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `email` be required.', async () => {
|
||||||
|
const res = await request().post('/api/users');
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const foundEmailParam = res.body.errors.find((error) => error.param === 'email');
|
||||||
|
expect(!!foundEmailParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be `email` be valid format.', async () => {
|
||||||
|
const user = make('user');
|
||||||
|
const res = await request().post('/api/users').send({
|
||||||
|
first_name: user.first_name,
|
||||||
|
last_name: user.last_name,
|
||||||
|
email: 'email',
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
status: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const foundEmailParam = res.body.errors.find((error) => error.param === 'email');
|
||||||
|
expect(!!foundEmailParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `phone_number` be valid format.', async () => {
|
||||||
|
const user = make('user');
|
||||||
|
const res = await request().post('/api/users').send({
|
||||||
|
first_name: user.first_name,
|
||||||
|
last_name: user.last_name,
|
||||||
|
email: user.email,
|
||||||
|
phone_number: 'phone_number',
|
||||||
|
status: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const phoneNumberParam = res.body.errors.find((error) => error.param === 'phone_number');
|
||||||
|
expect(!!phoneNumberParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `password` be required.', async () => {
|
||||||
|
const res = await request().post('/api/users').send();
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const passwordParam = res.body.errors.find((error) => error.param === 'password');
|
||||||
|
expect(!!passwordParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should password be equals confirm_password.', async () => {
|
||||||
|
const res = await request().post('/api/users').send({
|
||||||
|
password: '123123',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const passwordParam = res.body.errors.find((error) => error.param === 'password');
|
||||||
|
expect(!!passwordParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `status` be boolean', async () => {
|
||||||
|
const res = await request().post('/api/users').send({
|
||||||
|
status: 'not_boolean',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const statusParam = res.body.errors.find((error) => error.param === 'status');
|
||||||
|
expect(!!statusParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should response bad request in case email was already exist.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const res = await request().post('/api/users').send({
|
||||||
|
first_name: user.first_name,
|
||||||
|
last_name: user.last_name,
|
||||||
|
email: user.email,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
password: '123123123',
|
||||||
|
confirm_password: '123123123',
|
||||||
|
status: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(400);
|
||||||
|
expect(res.body.errors).include.something.that.deep.equals({
|
||||||
|
type: 'EMAIL_ALREADY_EXIST', code: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should response bad request in case phone number was already exist.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const res = await request().post('/api/users').send({
|
||||||
|
first_name: user.first_name,
|
||||||
|
last_name: user.last_name,
|
||||||
|
email: user.email,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
password: user.password,
|
||||||
|
confirm_password: user.password,
|
||||||
|
status: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(400);
|
||||||
|
expect(res.body.errors).include.something.that.deep.equals({
|
||||||
|
type: 'PHONE_NUMBER_ALREADY_EXIST', code: 120,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should response success with correct data type.', async () => {
|
||||||
|
const user = await make('user');
|
||||||
|
const res = await request().post('/api/users').send({
|
||||||
|
first_name: user.first_name,
|
||||||
|
last_name: user.last_name,
|
||||||
|
email: user.email,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
password: user.password,
|
||||||
|
confirm_password: user.password,
|
||||||
|
status: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST: `/users/:id`', () => {
|
||||||
|
it('Should create a new user if the user was not authorized.', () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should `password` be required.', () => {
|
it('Should `first_name` be required.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const res = await request().post(`/api/users/${user.id}`);
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const foundFirstNameParam = res.body.errors.find((error) => error.param === 'first_name');
|
||||||
|
expect(!!foundFirstNameParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `last_name` be required.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const res = await request().post(`/api/users/${user.id}`);
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const foundFirstNameParam = res.body.errors.find((error) => error.param === 'last_name');
|
||||||
|
expect(!!foundFirstNameParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `email` be required.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const res = await request().post(`/api/users/${user.id}`);
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const foundEmailParam = res.body.errors.find((error) => error.param === 'email');
|
||||||
|
expect(!!foundEmailParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be `email` be valid format.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const res = await request().post(`/api/users/${user.id}`).send({
|
||||||
|
first_name: user.first_name,
|
||||||
|
last_name: user.last_name,
|
||||||
|
email: 'email',
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
status: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const foundEmailParam = res.body.errors.find((error) => error.param === 'email');
|
||||||
|
expect(!!foundEmailParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `phone_number` be valid format.', async () => {
|
||||||
|
const user = create('user');
|
||||||
|
const res = await request().post(`/api/users/${user.id}`).send({
|
||||||
|
first_name: user.first_name,
|
||||||
|
last_name: user.last_name,
|
||||||
|
email: user.email,
|
||||||
|
phone_number: 'phone_number',
|
||||||
|
status: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const phoneNumberParam = res.body.errors.find((error) => error.param === 'phone_number');
|
||||||
|
expect(!!phoneNumberParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `password` be required.', async () => {
|
||||||
|
const user = create('user');
|
||||||
|
const res = await request().post(`/api/users/${user.id}`).send();
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const passwordParam = res.body.errors.find((error) => error.param === 'password');
|
||||||
|
expect(!!passwordParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should password be equals confirm_password.', async () => {
|
||||||
|
const user = create('user');
|
||||||
|
const res = await request().post(`/api/users/${user.id}`).send({
|
||||||
|
password: '123123',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const passwordParam = res.body.errors.find((error) => error.param === 'password');
|
||||||
|
expect(!!passwordParam).equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should `status` be boolean', async () => {
|
||||||
|
const user = create('user');
|
||||||
|
const res = await request().post(`/api/users/${user.id}`).send({
|
||||||
|
status: 'not_boolean',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(422);
|
||||||
|
|
||||||
|
const statusParam = res.body.errors.find((error) => error.param === 'status');
|
||||||
|
expect(!!statusParam).equals(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET: `/users/:id`', () => {
|
||||||
|
it('Should not success if the user was not authorized.', () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should `status` be boolean', () => {
|
it('Should response not found if the user was not exist.', async () => {
|
||||||
|
const res = await request().get('/api/users/10').send();
|
||||||
|
|
||||||
|
expect(res.status).equals(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should response success if the user was exist.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const res = await request().get(`/api/users/${user.id}`).send();
|
||||||
|
|
||||||
|
expect(res.status).equals(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,20 +280,26 @@ describe('routes: `/routes`', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not success if the user was pre-defined user.', () => {
|
it('Should response not found if the user was not exist.', async () => {
|
||||||
|
const res = await request().delete('/api/users/10').send();
|
||||||
|
|
||||||
|
expect(res.status).equals(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should response not found if the user was not exist.', () => {
|
it('Should response success if the user was exist.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
const res = await request().delete(`/api/users/${user.id}`).send();
|
||||||
|
|
||||||
|
expect(res.status).equals(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should response success if the user was exist.', () => {
|
it('Should delete the give user from the storage.', async () => {
|
||||||
|
const user = await create('user');
|
||||||
|
await request().delete(`/api/users/${user.id}`).send();
|
||||||
|
|
||||||
});
|
const storedUsers = await knex('users').where('id', user.id);
|
||||||
|
|
||||||
it('Should delete the give user after success response.', () => {
|
|
||||||
|
|
||||||
|
expect(storedUsers).to.have.lengthOf(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user