fix: resource advanced view filter.

This commit is contained in:
Ahmed Bouhuolia
2020-09-16 21:41:09 +02:00
parent a22c8395f3
commit ca92c925a9
72 changed files with 997 additions and 2324 deletions

View File

@@ -1,30 +1,21 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, validationResult, param, query } from 'express-validator';
import { difference } from 'lodash';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import JournalPoster from 'services/Accounting/JournalPoster';
import {
mapViewRolesToConditionals,
mapFilterRolesToDynamicFilter,
} from 'lib/ViewRolesBuilder';
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
} from 'lib/DynamicFilter';
import BaseController from './BaseController';
import { IAccountDTO, IAccount } from 'interfaces';
import { ServiceError } from 'exceptions';
import AccountsService from 'services/Accounts/AccountsService';
import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import BaseController from 'api/controllers/BaseController';
import AccountsService from 'services/Accounts/AccountsService';
import { IAccountDTO, IAccountsFilter } from 'interfaces';
import { ServiceError } from 'exceptions';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service()
export default class AccountsController extends BaseController{
@Inject()
accountsService: AccountsService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Router constructor method.
*/
@@ -39,13 +30,15 @@ export default class AccountsController extends BaseController{
'/:id/activate', [
...this.accountParamSchema,
],
asyncMiddleware(this.activateAccount.bind(this))
asyncMiddleware(this.activateAccount.bind(this)),
this.catchServiceErrors,
);
router.post(
'/:id/inactivate', [
...this.accountParamSchema,
],
asyncMiddleware(this.inactivateAccount.bind(this))
asyncMiddleware(this.inactivateAccount.bind(this)),
this.catchServiceErrors,
);
router.post(
'/:id', [
@@ -53,47 +46,48 @@ export default class AccountsController extends BaseController{
...this.accountParamSchema,
],
this.validationResult,
asyncMiddleware(this.editAccount.bind(this))
asyncMiddleware(this.editAccount.bind(this)),
this.catchServiceErrors,
);
router.post(
'/', [
...this.accountDTOSchema,
],
this.validationResult,
asyncMiddleware(this.newAccount.bind(this))
asyncMiddleware(this.newAccount.bind(this)),
this.catchServiceErrors,
);
router.get(
'/:id', [
...this.accountParamSchema,
],
this.validationResult,
asyncMiddleware(this.getAccount.bind(this))
asyncMiddleware(this.getAccount.bind(this)),
this.catchServiceErrors,
);
router.get(
'/', [
...this.accountsListSchema,
],
this.validationResult,
asyncMiddleware(this.getAccountsList.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors,
);
// // router.get(
// // '/', [
// // ...this.accountsListSchema
// // ],
// // asyncMiddleware(this.getAccountsList.handler)
// // );
router.delete(
'/:id', [
...this.accountParamSchema
],
this.validationResult,
asyncMiddleware(this.deleteAccount.bind(this))
asyncMiddleware(this.deleteAccount.bind(this)),
this.catchServiceErrors,
);
router.delete(
'/',
this.bulkDeleteSchema,
asyncMiddleware(this.deleteBulkAccounts.bind(this))
asyncMiddleware(this.deleteBulkAccounts.bind(this)),
this.catchServiceErrors,
);
// router.post(
// '/:id/transfer_account/:toAccount',
// this.transferToAnotherAccount.validation,
// asyncMiddleware(this.transferToAnotherAccount.handler)
// );
return router;
}
@@ -142,11 +136,7 @@ export default class AccountsController extends BaseController{
*/
get accountsListSchema() {
return [
query('display_type').optional().isIn(['tree', 'flat']),
query('account_types').optional().isArray(),
query('account_types.*').optional().isNumeric().toInt(),
query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(),
query('column_sort_by').optional(),
@@ -176,10 +166,7 @@ export default class AccountsController extends BaseController{
return res.status(200).send({ id: account.id });
} catch (error) {
if (error instanceof ServiceError) {
this.transformServiceErrorToResponse(res, error);
}
next();
next(error);
}
}
@@ -198,10 +185,7 @@ export default class AccountsController extends BaseController{
const account = await this.accountsService.editAccount(tenantId, accountId, accountDTO);
return res.status(200).send({ id: account.id });
} catch (error) {
if (error instanceof ServiceError) {
this.transformServiceErrorToResponse(res, error);
}
next();
next(error);
}
}
@@ -220,10 +204,7 @@ export default class AccountsController extends BaseController{
return res.status(200).send({ account });
} catch (error) {
if (error instanceof ServiceError) {
this.transformServiceErrorToResponse(res, error);
}
next();
next(error);
}
}
@@ -240,14 +221,8 @@ export default class AccountsController extends BaseController{
try {
await this.accountsService.deleteAccount(tenantId, accountId);
return res.status(200).send({ id: accountId });
} catch (error) {
if (error instanceof ServiceError) {
this.transformServiceErrorToResponse(res, error);
}
next();
next(error);
}
}
@@ -265,10 +240,7 @@ export default class AccountsController extends BaseController{
await this.accountsService.activateAccount(tenantId, accountId, true);
return res.status(200).send({ id: accountId });
} catch (error) {
if (error instanceof ServiceError) {
this.transformServiceErrorToResponse(res, error);
}
next();
next(error);
}
}
@@ -287,10 +259,7 @@ export default class AccountsController extends BaseController{
return res.status(200).send({ id: accountId });
} catch (error) {
if (error instanceof ServiceError) {
this.transformServiceErrorToResponse(res, error);
}
next();
next(error);
}
}
@@ -310,10 +279,7 @@ export default class AccountsController extends BaseController{
await this.accountsService.activateAccounts(tenantId, accountsIds, isActive)
return res.status(200).send({ ids: accountsIds });
} catch (error) {
if (error instanceof ServiceError) {
this.transformServiceErrorToResponse(res, error);
}
next();
next(error);
}
}
@@ -332,211 +298,117 @@ export default class AccountsController extends BaseController{
return res.status(200).send({ ids: accountsIds });
} catch (error) {
console.log(error);
next(error);
}
}
if (error instanceof ServiceError) {
this.transformServiceErrorToResponse(res, error);
}
next();
/**
* Retrieve accounts datatable list.
* @param {Request} req
* @param {Response} res
*/
async getAccountsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter: IAccountsFilter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'name',
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const accounts = await this.accountsService.getAccountsList(tenantId, filter);
return res.status(200).send({ accounts });
} catch (error) {
next(error);
}
}
/**
* Transforms service errors to response.
* @param {Error}
* @param {Request} req
* @param {Response} res
* @param {ServiceError} error
*/
transformServiceErrorToResponse(res: Response, error: ServiceError) {
console.log(error.errorType);
if (error.errorType === 'account_not_found') {
return res.boom.notFound(
'The given account not found.', {
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }] }
);
}
if (error.errorType === 'account_name_not_unqiue') {
return res.boom.badRequest(
'The given account not unique.',
{ errors: [{ type: 'ACCOUNT.NAME.NOT.UNIQUE', code: 150 }], }
);
}
if (error.errorType === 'account_type_not_found') {
return res.boom.badRequest(
'The given account type not found.', {
errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] }
);
}
if (error.errorType === 'account_type_not_allowed_to_changed') {
return res.boom.badRequest(
'Not allowed to change account type of the account.',
{ errors: [{ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 300 }] }
);
}
if (error.errorType === 'parent_account_not_found') {
return res.boom.badRequest(
'The parent account not found.',
{ errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 400 }] },
);
}
if (error.errorType === 'parent_has_different_type') {
return res.boom.badRequest(
'The parent account has different type.',
{ errors: [{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 500 }] }
);
}
if (error.errorType === 'account_code_not_unique') {
return res.boom.badRequest(
'The given account code is not unique.',
{ errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }] }
);
}
if (error.errorType === 'account_has_children') {
return res.boom.badRequest(
'You could not delete account has children.',
{ errors: [{ type: 'ACCOUNT.HAS.CHILD.ACCOUNTS', code: 700 }] }
);
}
if (error.errorType === 'account_has_associated_transactions') {
return res.boom.badRequest(
'You could not delete account has associated transactions.',
{ errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 800 }] }
);
}
if (error.errorType === 'account_predefined') {
return res.boom.badRequest(
'You could not delete predefined account',
{ errors: [{ type: 'ACCOUNT.PREDEFINED', code: 900 }] }
);
}
if (error.errorType === 'accounts_not_found') {
return res.boom.notFound(
'Some of the given accounts not found.',
{ errors: [{ type: 'SOME.ACCOUNTS.NOT_FOUND', code: 1000 }] },
);
}
if (error.errorType === 'predefined_accounts') {
return res.boom.badRequest(
'Some of the given accounts are predefined.',
{ errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] }
);
catchServiceErrors(error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'account_not_found') {
return res.boom.notFound(
'The given account not found.',
{ errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }] }
);
}
if (error.errorType === 'account_name_not_unqiue') {
return res.boom.badRequest(
'The given account not unique.',
{ errors: [{ type: 'ACCOUNT.NAME.NOT.UNIQUE', code: 150 }], }
);
}
if (error.errorType === 'account_type_not_found') {
return res.boom.badRequest(
'The given account type not found.', {
errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }]
}
);
}
if (error.errorType === 'account_type_not_allowed_to_changed') {
return res.boom.badRequest(
'Not allowed to change account type of the account.',
{ errors: [{ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 300 }] }
);
}
if (error.errorType === 'parent_account_not_found') {
return res.boom.badRequest(
'The parent account not found.',
{ errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 400 }] },
);
}
if (error.errorType === 'parent_has_different_type') {
return res.boom.badRequest(
'The parent account has different type.',
{ errors: [{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 500 }] }
);
}
if (error.errorType === 'account_code_not_unique') {
return res.boom.badRequest(
'The given account code is not unique.',
{ errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }] }
);
}
if (error.errorType === 'account_has_children') {
return res.boom.badRequest(
'You could not delete account has children.',
{ errors: [{ type: 'ACCOUNT.HAS.CHILD.ACCOUNTS', code: 700 }] }
);
}
if (error.errorType === 'account_has_associated_transactions') {
return res.boom.badRequest(
'You could not delete account has associated transactions.',
{ errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 800 }] }
);
}
if (error.errorType === 'account_predefined') {
return res.boom.badRequest(
'You could not delete predefined account',
{ errors: [{ type: 'ACCOUNT.PREDEFINED', code: 900 }] }
);
}
if (error.errorType === 'accounts_not_found') {
return res.boom.notFound(
'Some of the given accounts not found.',
{ errors: [{ type: 'SOME.ACCOUNTS.NOT_FOUND', code: 1000 }] },
);
}
if (error.errorType === 'predefined_accounts') {
return res.boom.badRequest(
'Some of the given accounts are predefined.',
{ errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] }
);
}
}
next(error)
}
// /**
// * Retrieve accounts list.
// */
// getAccountsList(req, res) {
// const validationErrors = validationResult(req);
// if (!validationErrors.isEmpty()) {
// return res.boom.badData(null, {
// code: 'validation_error',
// ...validationErrors,
// });
// }
// const filter = {
// display_type: 'flat',
// account_types: [],
// filter_roles: [],
// sort_order: 'asc',
// ...req.query,
// };
// if (filter.stringified_filter_roles) {
// filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
// }
// const { Resource, Account, View } = req.models;
// const errorReasons = [];
// const accountsResource = await Resource.query()
// .remember()
// .where('name', 'accounts')
// .withGraphFetched('fields')
// .first();
// if (!accountsResource) {
// return res.status(400).send({
// errors: [{ type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200 }],
// });
// }
// const resourceFieldsKeys = accountsResource.fields.map((c) => c.key);
// const view = await View.query().onBuild((builder) => {
// if (filter.custom_view_id) {
// builder.where('id', filter.custom_view_id);
// } else {
// builder.where('favourite', true);
// }
// // builder.where('resource_id', accountsResource.id);
// builder.withGraphFetched('roles.field');
// builder.withGraphFetched('columns');
// builder.first();
// builder.remember();
// });
// const dynamicFilter = new DynamicFilter(Account.tableName);
// if (filter.column_sort_by) {
// if (resourceFieldsKeys.indexOf(filter.column_sort_by) === -1) {
// errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
// }
// const sortByFilter = new DynamicFilterSortBy(
// filter.column_sort_by,
// filter.sort_order
// );
// dynamicFilter.setFilter(sortByFilter);
// }
// // View roles.
// if (view && view.roles.length > 0) {
// const viewFilter = new DynamicFilterViews(
// mapViewRolesToConditionals(view.roles),
// view.rolesLogicExpression
// );
// if (!viewFilter.validateFilterRoles()) {
// errorReasons.push({
// type: 'VIEW.LOGIC.EXPRESSION.INVALID',
// code: 400,
// });
// }
// dynamicFilter.setFilter(viewFilter);
// }
// // Filter roles.
// if (filter.filter_roles.length > 0) {
// // Validate the accounts resource fields.
// const filterRoles = new DynamicFilterFilterRoles(
// mapFilterRolesToDynamicFilter(filter.filter_roles),
// accountsResource.fields
// );
// dynamicFilter.setFilter(filterRoles);
// if (filterRoles.validateFilterRoles().length > 0) {
// errorReasons.push({
// type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS',
// code: 500,
// });
// }
// }
// if (errorReasons.length > 0) {
// return res.status(400).send({ errors: errorReasons });
// }
// const accounts = await Account.query().onBuild((builder) => {
// builder.modify('filterAccountTypes', filter.account_types);
// builder.withGraphFetched('type');
// builder.withGraphFetched('balance');
// dynamicFilter.buildQuery()(builder);
// });
// return res.status(200).send({
// accounts:
// filter.display_type === 'tree'
// ? Account.toNestedArray(accounts)
// : accounts,
// ...(view
// ? {
// customViewId: view.id,
// }
// : {}),
// });
// }
};

View File

@@ -1,17 +1,28 @@
import { Response, Request } from 'express';
import { Response, Request, NextFunction } from 'express';
import { matchedData, validationResult } from "express-validator";
import { camelCase, omit } from "lodash";
import { mapKeysDeep } from 'utils'
export default class BaseController {
private dataToCamelCase(data) {
return mapKeysDeep(data, (v, k) => camelCase(k));
}
matchedBodyData(req: Request, options: any = {}) {
const data = matchedData(req, {
locations: ['body'],
includeOptionals: true,
...omit(options, ['locations']), // override any propery except locations.
});
return mapKeysDeep(data, (v, k) => camelCase(k));
return this.dataToCamelCase(data);
}
matchedQueryData(req: Request) {
const data = matchedData(req, {
locations: ['query'],
});
return this.dataToCamelCase(data);
}
validationResult(req: Request, res: Response, next: NextFunction) {

View File

@@ -1,10 +1,10 @@
import { Request, Response, Router, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { check } from 'express-validator';
import { check, query } from 'express-validator';
import ContactsController from 'api/controllers/Contacts/Contacts';
import VendorsService from 'services/Contacts/VendorsService';
import { ServiceError } from 'exceptions';
import { IVendorNewDTO, IVendorEditDTO } from 'interfaces';
import { IVendorNewDTO, IVendorEditDTO, IVendorsFilter } from 'interfaces';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
@Service()
@@ -52,6 +52,12 @@ export default class VendorsController extends ContactsController {
this.validationResult,
asyncMiddleware(this.getVendor.bind(this))
);
router.get('/', [
...this.vendorsListSchema,
],
this.validationResult,
asyncMiddleware(this.getVendorsList.bind(this)),
);
return router;
}
@@ -64,6 +70,22 @@ export default class VendorsController extends ContactsController {
];
}
/**
* Vendors datatable list validation schema.
*/
get vendorsListSchema() {
return [
query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
];
}
/**
* Creates a new vendor.
* @param {Request} req
@@ -192,4 +214,24 @@ export default class VendorsController extends ContactsController {
next(error);
}
}
/**
* Retrieve vendors datatable list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getVendorsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const vendorsFilter: IVendorsFilter = {
filterRoles: [],
...this.matchedBodyData(req),
};
try {
const vendors = await this.vendorsService.getVendorsList(tenantId, vendorsFilter);
return res.status(200).send({ vendors });
} catch (error) {
}
}
}

View File

@@ -6,12 +6,16 @@ import BaseController from "api/controllers/BaseController";
import ExpensesService from "services/Expenses/ExpensesService";
import { IExpenseDTO } from 'interfaces';
import { ServiceError } from "exceptions";
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service()
export default class ExpensesController extends BaseController {
@Inject()
expensesService: ExpensesService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Express router.
*/
@@ -23,19 +27,22 @@ export default class ExpensesController extends BaseController {
...this.expenseDTOSchema,
],
this.validationResult,
asyncMiddleware(this.newExpense.bind(this))
asyncMiddleware(this.newExpense.bind(this)),
this.catchServiceErrors,
);
router.post('/publish', [
...this.bulkSelectSchema,
],
this.bulkPublishExpenses.bind(this)
this.bulkPublishExpenses.bind(this),
this.catchServiceErrors,
);
router.post(
'/:id/publish', [
...this.expenseParamSchema,
],
this.validationResult,
asyncMiddleware(this.publishExpense.bind(this))
asyncMiddleware(this.publishExpense.bind(this)),
this.catchServiceErrors,
);
router.post(
'/:id', [
@@ -44,19 +51,28 @@ export default class ExpensesController extends BaseController {
],
this.validationResult,
asyncMiddleware(this.editExpense.bind(this)),
this.catchServiceErrors,
);
router.delete(
'/:id', [
...this.expenseParamSchema,
],
this.validationResult,
asyncMiddleware(this.deleteExpense.bind(this))
asyncMiddleware(this.deleteExpense.bind(this)),
this.catchServiceErrors,
);
router.delete('/', [
...this.bulkSelectSchema,
],
this.validationResult,
asyncMiddleware(this.bulkDeleteExpenses.bind(this))
asyncMiddleware(this.bulkDeleteExpenses.bind(this)),
this.catchServiceErrors,
);
router.get(
'/',
asyncMiddleware(this.getExpensesList.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors,
);
return router;
}
@@ -118,9 +134,6 @@ export default class ExpensesController extends BaseController {
const expense = await this.expensesService.newExpense(tenantId, expenseDTO, user);
return res.status(200).send({ id: expense.id });
} catch (error) {
if (error instanceof ServiceError) {
this.serviceErrorsTransformer(res, error);
}
next(error);
}
}
@@ -140,9 +153,6 @@ export default class ExpensesController extends BaseController {
await this.expensesService.editExpense(tenantId, expenseId, expenseDTO, user);
return res.status(200).send({ id: expenseId });
} catch (error) {
if (error instanceof ServiceError) {
this.serviceErrorsTransformer(res, error);
}
next(error)
}
}
@@ -161,9 +171,6 @@ export default class ExpensesController extends BaseController {
await this.expensesService.deleteExpense(tenantId, expenseId)
return res.status(200).send({ id: expenseId });
} catch (error) {
if (error instanceof ServiceError) {
this.serviceErrorsTransformer(res, error);
}
next(error)
}
}
@@ -182,9 +189,6 @@ export default class ExpensesController extends BaseController {
await this.expensesService.publishExpense(tenantId, expenseId)
return res.status(200).send({ });
} catch (error) {
if (error instanceof ServiceError) {
this.serviceErrorsTransformer(req, error);
}
next(error);
}
}
@@ -203,9 +207,6 @@ export default class ExpensesController extends BaseController {
await this.expensesService.deleteBulkExpenses(tenantId, expensesIds);
return res.status(200).send({ ids: expensesIds });
} catch (error) {
if (error instanceof ServiceError) {
this.serviceErrorsTransformer(req, error);
}
next(error);
}
}
@@ -218,9 +219,23 @@ export default class ExpensesController extends BaseController {
await this.expensesService.publishBulkExpenses(tenantId,);
return res.status(200).send({});
} catch (error) {
if (error instanceof ServiceError) {
this.serviceErrorsTransformer(req, error);
}
next(error);
}
}
async getExpensesList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
...this.matchedQueryData(req),
};
try {
const expenses = await this.expensesService.getExpensesList(tenantId, filter);
return res.status(200).send({ expenses });
} catch (error) {
next(error);
}
}
@@ -230,36 +245,39 @@ export default class ExpensesController extends BaseController {
* @param {Response} res
* @param {ServiceError} error
*/
serviceErrorsTransformer(res, error: ServiceError) {
if (error.errorType === 'expense_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'EXPENSE_NOT_FOUND' }],
});
}
if (error.errorType === 'total_amount_equals_zero') {
return res.boom.badRequest(null, {
errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO' }],
});
}
if (error.errorType === 'payment_account_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', }],
});
}
if (error.errorType === 'some_expenses_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 200 }]
})
}
if (error.errorType === 'payment_account_has_invalid_type') {
return res.boom.badRequest(null, {
errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE' }],
});
}
if (error.errorType === 'expenses_account_has_invalid_type') {
return res.boom.badRequest(null, {
errors: [{ type: 'EXPENSES.ACCOUNT.HAS.INVALID.TYPE' }]
});
catchServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'expense_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'EXPENSE_NOT_FOUND' }],
});
}
if (error.errorType === 'total_amount_equals_zero') {
return res.boom.badRequest(null, {
errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO' }],
});
}
if (error.errorType === 'payment_account_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', }],
});
}
if (error.errorType === 'some_expenses_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 200 }]
})
}
if (error.errorType === 'payment_account_has_invalid_type') {
return res.boom.badRequest(null, {
errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE' }],
});
}
if (error.errorType === 'expenses_account_has_invalid_type') {
return res.boom.badRequest(null, {
errors: [{ type: 'EXPENSES.ACCOUNT.HAS.INVALID.TYPE' }]
});
}
}
next(error);
}
}

View File

@@ -8,14 +8,6 @@ import { difference } from 'lodash';
import { Service } from 'typedi';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import validateMiddleware from 'api/middleware/validateMiddleware';
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterFilterRoles,
} from 'lib/DynamicFilter';
import {
mapFilterRolesToDynamicFilter,
} from 'lib/ViewRolesBuilder';
import { IItemCategory, IItemCategoryOTD } from 'interfaces';
import BaseController from 'api/controllers/BaseController';
@@ -336,67 +328,7 @@ export default class ItemsCategoriesController extends BaseController {
* @return {Response}
*/
async getList(req: Request, res: Response) {
const { Resource, ItemCategory } = req.models;
const categoriesResource = await Resource.query()
.where('name', 'items_categories')
.withGraphFetched('fields')
.first();
if (!categoriesResource) {
return res.status(400).send({
errors: [{ type: 'ITEMS.CATEGORIES.RESOURCE.NOT.FOUND', code: 200 }],
});
}
const filter = {
column_sort_order: '',
sort_order: '',
filter_roles: [],
...req.query,
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const errorReasons = [];
const resourceFieldsKeys = categoriesResource.fields.map((c) => c.key);
const dynamicFilter = new DynamicFilter(ItemCategory.tableName);
// Dynamic filter with filter roles.
if (filter.filter_roles.length > 0) {
// Validate the accounts resource fields.
const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles),
categoriesResource.fields,
);
categoriesResource.setFilter(filterRoles);
if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'ITEMS.RESOURCE.HAS.NO.FIELDS', code: 500 });
}
}
// Dynamic filter with column sort order.
if (filter.column_sort_order) {
if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) {
errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
}
const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_order,
filter.sort_order,
);
dynamicFilter.setFilter(sortByFilter);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const categories = await ItemCategory.query().onBuild((builder) => {
dynamicFilter.buildQuery()(builder);
builder.select([
'*',
ItemCategory.relatedQuery('items').count().as('count'),
]);
});
return res.status(200).send({ categories });
}
/**

View File

@@ -1,18 +1,20 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response } from 'express';
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, ValidationChain, matchedData } from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import validateMiddleware from 'api/middleware/validateMiddleware';
import ItemsService from 'services/Items/ItemsService';
import DynamicListing from 'services/DynamicListing/DynamicListing';
import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder';
import { dynamicListingErrorsToResponse } from 'services/DynamicListing/hasDynamicListing';
import BaseController from 'api/controllers/BaseController';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service()
export default class ItemsController {
export default class ItemsController extends BaseController {
@Inject()
itemsService: ItemsService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Router constructor.
*/
@@ -62,7 +64,8 @@ export default class ItemsController {
'/',
this.validateListQuerySchema,
validateMiddleware,
asyncMiddleware(this.listItems.bind(this)),
asyncMiddleware(this.getItemsList.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
);
return router;
}
@@ -131,7 +134,7 @@ export default class ItemsController {
*/
get validateListQuerySchema() {
return [
query('column_sort_order').optional().isIn(['created_at', 'name', 'amount', 'sku']),
query('column_sort_order').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
@@ -353,79 +356,25 @@ export default class ItemsController {
}
/**
* Listing items with pagination metadata.
* Retrieve items datatable list.
* @param {Request} req
* @param {Response} res
*/
async listItems(req: Request, res: Response) {
async getItemsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filter_roles: [],
sort_order: 'asc',
page: 1,
page_size: 10,
...req.query,
filterRoles: [],
sortOrder: 'asc',
...this.matchedQueryData(req),
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
const { Resource, Item, View } = req.models;
const resource = await Resource.query()
.remember()
.where('name', 'items')
.withGraphFetched('fields')
.first();
if (!resource) {
return res.status(400).send({
errors: [{ type: 'ITEMS.RESOURCE.NOT_FOUND', code: 200 }],
});
try {
const items = await this.itemsService.getItemsList(tenantId, filter);
return res.status(200).send({ items });
} catch (error) {
next(error);
}
const viewMeta = await View.query()
.modify('allMetadata')
.modify('specificOrFavourite', filter.custom_view_id)
.where('resource_id', resource.id)
.first();
const listingBuilder = new DynamicListingBuilder();
const errorReasons = [];
listingBuilder.addModelClass(Item);
listingBuilder.addCustomViewId(filter.custom_view_id);
listingBuilder.addFilterRoles(filter.filter_roles);
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
listingBuilder.addView(viewMeta);
const dynamicListing = new DynamicListing(listingBuilder);
if (dynamicListing instanceof Error) {
const errors = dynamicListingErrorsToResponse(dynamicListing);
errorReasons.push(...errors);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const items = await Item.query().onBuild((builder: any) => {
builder.withGraphFetched('costAccount');
builder.withGraphFetched('sellAccount');
builder.withGraphFetched('inventoryAccount');
builder.withGraphFetched('category');
dynamicListing.buildQuery()(builder);
return builder;
}).pagination(filter.page - 1, filter.page_size);
return res.status(200).send({
items: {
...items,
...(viewMeta
? {
viewMeta: {
custom_view_id: viewMeta.id,
view_columns: viewMeta.columns,
}
}
: {}),
},
});
}
}
}

View File

@@ -68,16 +68,15 @@ export default class OrganizationController extends BaseController{
});
}
}
console.log(error);
next(error);
}
}
/**
* Seeds initial data to tenant database.
* @param req
* @param res
* @param next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async seed(req: Request, res: Response, next: Function) {
const { organizationId } = req.tenant;

View File

@@ -9,9 +9,6 @@ import BillsService from 'services/Purchases/Bills';
import BaseController from 'api/controllers/BaseController';
import ItemsService from 'services/Items/ItemsService';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder';
import DynamicListing from 'services/DynamicListing/DynamicListing';
import { dynamicListingErrorsToResponse } from 'services/DynamicListing/HasDynamicListing';
@Service()
export default class BillsController extends BaseController {
@@ -330,71 +327,6 @@ export default class BillsController extends BaseController {
* @return {Response}
*/
async listingBills(req: Request, res: Response) {
const filter = {
filter_roles: [],
sort_order: 'asc',
page: 1,
page_size: 10,
...req.query,
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const { Bill, View, Resource } = req.models;
const resource = await Resource.query()
.remember()
.where('name', 'bills')
.withGraphFetched('fields')
.first();
if (!resource) {
return res.status(400).send({
errors: [{ type: 'BILLS_RESOURCE_NOT_FOUND', code: 200 }],
});
}
const viewMeta = await View.query()
.modify('allMetadata')
.modify('specificOrFavourite', filter.custom_view_id)
.where('resource_id', resource.id)
.first();
const listingBuilder = new DynamicListingBuilder();
const errorReasons = [];
listingBuilder.addModelClass(Bill);
listingBuilder.addCustomViewId(filter.custom_view_id);
listingBuilder.addFilterRoles(filter.filter_roles);
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
listingBuilder.addView(viewMeta);
const dynamicListing = new DynamicListing(listingBuilder);
if (dynamicListing instanceof Error) {
const errors = dynamicListingErrorsToResponse(dynamicListing);
errorReasons.push(...errors);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const bills = await Bill.query()
.onBuild((builder) => {
dynamicListing.buildQuery()(builder);
builder.withGraphFetched('vendor');
return builder;
})
.pagination(filter.page - 1, filter.page_size);
return res.status(200).send({
bills: {
...bills,
...(viewMeta
? {
view_meta: {
customViewId: viewMeta.id,
},
}
: {}),
},
});
}
}

View File

@@ -1,5 +1,5 @@
import { Router } from 'express';
import { Router, Request, Response } from 'express';
import { Service, Inject } from 'typedi';
import { check, param, query, ValidationChain, matchedData } from 'express-validator';
import { difference } from 'lodash';
@@ -8,9 +8,6 @@ import validateMiddleware from 'api/middleware/validateMiddleware';
import BaseController from 'api/controllers/BaseController';
import BillPaymentsService from 'services/Purchases/BillPayments';
import AccountsService from 'services/Accounts/AccountsService';
import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder';
import DynamicListing from 'services/DynamicListing/DynamicListing';
import { dynamicListingErrorsToResponse } from 'services/DynamicListing/hasDynamicListing';
/**
* Bills payments controller.
@@ -384,70 +381,6 @@ export default class BillsPayments extends BaseController {
* @return {Response}
*/
async getBillsPayments(req: Request, res: Response) {
const filter = {
filter_roles: [],
sort_order: 'asc',
page: 1,
page_size: 10,
...req.query,
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const { BillPayment, View, Resource } = req.models;
const resource = await Resource.query()
.where('name', 'bill_payments')
.withGraphFetched('fields')
.first();
if (!resource) {
return res.status(400).send({
errors: [{ type: 'BILL.PAYMENTS.RESOURCE.NOT_FOUND', code: 200 }],
});
}
const viewMeta = await View.query()
.modify('allMetadata')
.modify('specificOrFavourite', filter.custom_view_id)
.where('resource_id', resource.id)
.first();
const listingBuilder = new DynamicListingBuilder();
const errorReasons = [];
listingBuilder.addModelClass(BillPayment);
listingBuilder.addCustomViewId(filter.custom_view_id);
listingBuilder.addFilterRoles(filter.filter_roles);
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
listingBuilder.addView(viewMeta);
const dynamicListing = new DynamicListing(listingBuilder);
if (dynamicListing instanceof Error) {
const errors = dynamicListingErrorsToResponse(dynamicListing);
errorReasons.push(...errors);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const billPayments = await BillPayment.query().onBuild((builder) => {
dynamicListing.buildQuery()(builder);
builder.withGraphFetched('vendor');
builder.withGraphFetched('paymentAccount');
return builder;
}).pagination(filter.page - 1, filter.page_size);
return res.status(200).send({
bill_payments: {
...billPayments,
...(viewMeta
? {
view_meta: {
customViewId: viewMeta.id,
},
}
: {}),
},
});
}
}

View File

@@ -9,9 +9,6 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware';
import PaymentReceiveService from 'services/Sales/PaymentsReceives';
import SaleInvoiceService from 'services/Sales/SalesInvoices';
import AccountsService from 'services/Accounts/AccountsService';
import DynamicListing from 'services/DynamicListing/DynamicListing';
import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder';
import { dynamicListingErrorsToResponse } from 'services/DynamicListing/hasDynamicListing';
/**
* Payments receives controller.
@@ -406,71 +403,6 @@ export default class PaymentReceivesController extends BaseController {
* @return {Response}
*/
async getPaymentReceiveList(req: Request, res: Response) {
const filter = {
filter_roles: [],
sort_order: 'asc',
page: 1,
page_size: 10,
...req.query,
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const { Resource, PaymentReceive, View, Bill } = req.models;
const resource = await Resource.query()
.remember()
.where('name', 'payment_receives')
.withGraphFetched('fields')
.first();
if (!resource) {
return res.status(400).send({
errors: [{ type: 'PAYMENT_RECEIVES_RESOURCE_NOT_FOUND', code: 200 }],
});
}
const viewMeta = await View.query()
.modify('allMetadata')
.modify('specificOrFavourite', filter.custom_view_id)
.where('resource_id', resource.id)
.first();
const listingBuilder = new DynamicListingBuilder();
const errorReasons = [];
listingBuilder.addModelClass(Bill);
listingBuilder.addCustomViewId(filter.custom_view_id);
listingBuilder.addFilterRoles(filter.filter_roles);
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
listingBuilder.addView(viewMeta);
const dynamicListing = new DynamicListing(listingBuilder);
if (dynamicListing instanceof Error) {
const errors = dynamicListingErrorsToResponse(dynamicListing);
errorReasons.push(...errors);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const paymentReceives = await PaymentReceive.query().onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
dynamicListing.buildQuery()(builder);
return builder;
}).pagination(filter.page - 1, filter.page_size);
return res.status(200).send({
payment_receives: {
...paymentReceives,
...(viewMeta
? {
viewMeta: {
customViewId: viewMeta.id,
}
}
: {}),
},
});
}
}

View File

@@ -7,8 +7,6 @@ import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import ItemsService from 'services/Items/ItemsService';
import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder';
import DynamicListing from 'services/DynamicListing/DynamicListing';
@Service()
export default class SalesEstimatesController extends BaseController {
@@ -301,69 +299,6 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res
*/
async getEstimates(req: Request, res: Response) {
const filter = {
filter_roles: [],
sort_order: 'asc',
page: 1,
page_size: 10,
...req.query,
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const { SaleEstimate, Resource, View } = req.models;
const resource = await Resource.query()
.remember()
.where('name', 'sales_estimates')
.withGraphFetched('fields')
.first();
if (!resource) {
return res.status(400).send({
errors: [{ type: 'RESOURCE.NOT.FOUND', code: 200, }],
});
}
const viewMeta = await View.query()
.modify('allMetadata')
.modify('specificOrFavourite', filter.custom_view_id)
.where('resource_id', resource.id)
.first();
const listingBuilder = new DynamicListingBuilder();
const errorReasons = [];
listingBuilder.addView(viewMeta);
listingBuilder.addModelClass(SaleEstimate);
listingBuilder.addCustomViewId(filter.custom_view_id);
listingBuilder.addFilterRoles(filter.filter_roles);
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
const dynamicListing = new DynamicListing(listingBuilder);
if (dynamicListing instanceof Error) {
const errors = dynamicListingErrorsToResponse(dynamicListing);
errorReasons.push(...errors);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const salesEstimates = await SaleEstimate.query().onBuild((builder) => {
dynamicListing.buildQuery()(builder);
builder.withGraphFetched('customer');
return builder;
}).pagination(filter.page - 1, filter.page_size);
return res.status(200).send({
sales_estimates: {
...salesEstimates,
...(viewMeta ? {
viewMeta: {
custom_view_id: viewMeta.id,
},
} : {}),
},
});
}
};

View File

@@ -7,9 +7,6 @@ import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleInvoiceService from 'services/Sales/SalesInvoices';
import ItemsService from 'services/Items/ItemsService';
import DynamicListing from 'services/DynamicListing/DynamicListing';
import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder';
import { dynamicListingErrorsToResponse } from 'services/DynamicListing/hasDynamicListing';
import { ISaleInvoiceOTD } from 'interfaces';
@Service()
@@ -415,69 +412,6 @@ export default class SaleInvoicesController {
* @param {Function} next
*/
async getSalesInvoices(req, res) {
const filter = {
filter_roles: [],
sort_order: 'asc',
page: 1,
page_size: 10,
...req.query,
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const { SaleInvoice, View, Resource } = req.models;
const resource = await Resource.query()
.remember()
.where('name', 'sales_invoices')
.withGraphFetched('fields')
.first();
if (!resource) {
return res.status(400).send({
errors: [{ type: 'SALES_INVOICES_RESOURCE_NOT_FOUND', code: 200 }],
});
}
const viewMeta = await View.query()
.modify('allMetadata')
.modify('specificOrFavourite', filter.custom_view_id)
.where('resource_id', resource.id)
.first();
const listingBuilder = new DynamicListingBuilder();
const errorReasons = [];
listingBuilder.addModelClass(SaleInvoice);
listingBuilder.addCustomViewId(filter.custom_view_id);
listingBuilder.addFilterRoles(filter.filter_roles);
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
listingBuilder.addView(viewMeta);
const dynamicListing = new DynamicListing(listingBuilder);
if (dynamicListing instanceof Error) {
const errors = dynamicListingErrorsToResponse(dynamicListing);
errorReasons.push(...errors);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const salesInvoices = await SaleInvoice.query().onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('customer');
dynamicListing.buildQuery()(builder);
}).pagination(filter.page - 1, filter.page_size);
return res.status(200).send({
sales_invoices: {
...salesInvoices,
...(viewMeta
? {
view_meta: {
customViewId: viewMeta.id,
}
}
: {}),
},
});
}
}

View File

@@ -6,11 +6,6 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware';
import AccountsService from 'services/Accounts/AccountsService';
import ItemsService from 'services/Items/ItemsService';
import SaleReceiptService from 'services/Sales/SalesReceipts';
import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder';
import DynamicListing from 'services/DynamicListing/DynamicListing';
import {
dynamicListingErrorsToResponse
} from 'services/DynamicListing/HasDynamicListing';
@Service()
export default class SalesReceiptsController {
@@ -302,65 +297,6 @@ export default class SalesReceiptsController {
* @param {Response} res
*/
async listingSalesReceipts(req: Request, res: Response) {
const filter = {
filter_roles: [],
sort_order: 'asc',
page: 1,
page_size: 10,
...req.query,
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const { SaleReceipt, Resource, View } = req.models;
const resource = await Resource.query()
.remember()
.where('name', 'sales_receipts')
.withGraphFetched('fields')
.first();
if (!resource) {
return res.status(400).send({
errors: [{ type: 'RESOURCE.NOT.FOUND', code: 200, }],
});
}
const viewMeta = await View.query()
.modify('allMetadata')
.modify('specificOrFavourite', filter.custom_view_id)
.where('resource_id', resource.id)
.first();
const listingBuilder = new DynamicListingBuilder();
const errorReasons = [];
listingBuilder.addView(viewMeta);
listingBuilder.addModelClass(SaleReceipt);
listingBuilder.addCustomViewId(filter.custom_view_id);
listingBuilder.addFilterRoles(filter.filter_roles);
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
const dynamicListing = new DynamicListing(listingBuilder);
if (dynamicListing instanceof Error) {
const errors = dynamicListingErrorsToResponse(dynamicListing);
errorReasons.push(...errors);
}
const salesReceipts = await SaleReceipt.query().onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
builder.withGraphFetched('entries');
dynamicListing.buildQuery()(builder);
}).pagination(filter.page - 1, filter.page_size);
return res.status(200).send({
sales_receipts: {
...salesReceipts,
...(viewMeta ? {
view_meta: {
customViewId: viewMeta.id,
}
} : {}),
},
});
}
};

View File

@@ -178,7 +178,6 @@ export default class LicensesController extends BaseController {
message: 'The licenses have been generated successfully.'
});
} catch (error) {
console.log(error);
next(error);
}
}

View File

@@ -134,7 +134,6 @@ export default class UsersController extends BaseController{
await this.usersService.deleteUser(tenantId, id);
return res.status(200).send({ id });
} catch (error) {
if (error instanceof ServiceError) {
if (error.errorType === 'user_not_found') {
return res.boom.notFound(null, {
@@ -142,7 +141,7 @@ export default class UsersController extends BaseController{
});
}
}
next();
next(error);
}
}
@@ -160,8 +159,6 @@ export default class UsersController extends BaseController{
const user = await this.usersService.getUser(tenantId, userId);
return res.status(200).send({ user });
} catch (error) {
console.log(error);
if (error instanceof ServiceError) {
if (error.errorType === 'user_not_found') {
return res.boom.notFound(null, {
@@ -169,7 +166,7 @@ export default class UsersController extends BaseController{
});
}
}
next();
next(error);
}
}
@@ -186,7 +183,7 @@ export default class UsersController extends BaseController{
return res.status(200).send({ users });
} catch (error) {
next();
next(error);
}
}
@@ -204,8 +201,6 @@ export default class UsersController extends BaseController{
await this.usersService.activateUser(tenantId, userId);
return res.status(200).send({ id: userId });
} catch(error) {
console.log(error);
if (error instanceof ServiceError) {
if (error.errorType === 'user_not_found') {
return res.status(404).send({
@@ -218,7 +213,7 @@ export default class UsersController extends BaseController{
});
}
}
next();
next(error);
}
}
@@ -248,7 +243,7 @@ export default class UsersController extends BaseController{
});
}
}
next();
next(error);
}
}
};

View File

@@ -6,8 +6,6 @@ export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantId } = req.user;
const { knex } = req;
console.log(knex);
const Logger = Container.get('logger');
const tenantContainer = Container.of(`tenant-${tenantId}`);

View File

@@ -37,7 +37,7 @@ export default {
'type': {
column: 'account_type_id',
relation: 'account_types.id',
relationColumn: 'account_types.id',
relationColumn: 'account_types.key',
},
'description': {
column: 'description',
@@ -58,7 +58,8 @@ export default {
column: 'active',
},
balance: {
column: 'balance',
column: 'amount',
columnType: 'number'
},
currency: {
column: 'currency_code',

View File

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

View File

@@ -1,21 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable('resource_fields', (table) => {
table.increments();
table.string('label_name');
table.string('key');
table.string('data_type');
table.string('help_text');
table.string('default');
table.boolean('active');
table.boolean('predefined');
table.boolean('builtin').defaultTo(false);
table.boolean('columnable');
table.integer('index');
table.string('data_resource');
table.json('options');
table.integer('resource_id').unsigned();
}).raw('ALTER TABLE `RESOURCE_FIELDS` AUTO_INCREMENT = 1000');
};
exports.down = (knex) => knex.schema.dropTableIfExists('resource_fields');

View File

@@ -3,7 +3,7 @@ exports.up = function (knex) {
return knex.schema.createTable('view_has_columns', (table) => {
table.increments();
table.integer('view_id').unsigned();
table.integer('field_id').unsigned();
table.string('field_key');
table.integer('index').unsigned();
}).raw('ALTER TABLE `ITEMS_CATEGORIES` AUTO_INCREMENT = 1000');
};

View File

@@ -0,0 +1,13 @@
exports.up = function (knex) {
return knex.schema.createTable('view_roles', (table) => {
table.increments();
table.integer('index');
table.string('field_key');
table.string('comparator');
table.string('value');
table.integer('view_id').unsigned();
}).raw('ALTER TABLE `VIEW_ROLES` AUTO_INCREMENT = 1000');
};
exports.down = (knex) => knex.schema.dropTableIfExists('view_roles');

View File

@@ -4,7 +4,7 @@ exports.up = function (knex) {
table.increments();
table.string('name');
table.boolean('predefined');
table.integer('resource_id').unsigned().references('id').inTable('resources');
table.string('resource_model');
table.boolean('favourite');
table.string('roles_logic_expression');
table.timestamps();

View File

@@ -1,14 +0,0 @@
exports.up = function(knex) {
return knex.schema.createTable('resource_custom_fields_metadata', (table) => {
table.increments();
table.integer('resource_id').unsigned();
table.integer('resource_item_id').unsigned();
table.string('key');
table.string('value');
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('resource_custom_fields_metadata');
};

View File

@@ -0,0 +1,37 @@
exports.up = (knex) => {
// Deletes ALL existing entries
return knex('views').del()
.then(() => {
// Inserts seed entries
return knex('views').insert([
// Accounts
{ id: 15, name: 'Inactive', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
{ id: 1, name: 'Assets', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
{ id: 2, name: 'Liabilities', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
{ id: 3, name: 'Equity', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
{ id: 4, name: 'Income', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
{ id: 5, name: 'Expenses', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
// Items
// { id: 6, name: 'Services', roles_logic_expression: '1', resource_id: 2, predefined: true },
// { id: 7, name: 'Inventory', roles_logic_expression: '1', resource_id: 2, predefined: true },
// { id: 8, name: 'Non-Inventory', roles_logic_expression: '1', resource_id: 2, predefined: true },
// // Manual Journals
// { id: 9, name: 'Journal', roles_logic_expression: '1', resource_id: 4, predefined: true },
// { id: 10, name: 'Credit', roles_logic_expression: '1', resource_id: 4, predefined: true },
// { id: 11, name: 'Reconciliation', roles_logic_expression: '1', resource_id: 4, predefined: true },
// // Expenses
// { id: 12, name: 'Interest', roles_logic_expression: '1', resource_id: 3, predefined: false, },
// { id: 13, name: 'Depreciation', roles_logic_expression: '1', resource_id: 3, predefined: false, },
// { id: 14, name: 'Payroll', roles_logic_expression: '1', resource_id: '3', predefined: false },
]);
});
};
exports.down = (knex) => {
};

View File

@@ -0,0 +1,34 @@
exports.up = (knex) => {
// Deletes ALL existing entries
return knex('view_roles').del()
.then(() => {
// Inserts seed entries
return knex('view_roles').insert([
// Accounts
{ id: 1, field_key: 'type', index: 1, comparator: 'equals', value: 'asset', view_id: 1 },
{ id: 2, field_key: 'type', index: 1, comparator: 'equals', value: 'liability', view_id: 2 },
{ id: 3, field_key: 'type', index: 1, comparator: 'equals', value: 'equity', view_id: 3 },
{ id: 4, field_key: 'type', index: 1, comparator: 'equals', value: 'income', view_id: 4 },
{ id: 5, field_key: 'type', index: 1, comparator: 'equals', value: 'expense', view_id: 5 },
{ id: 12, field_key: 'active', index: 1, comparator: 'is', value: 1, view_id: 15 },
// Items.
// { id: 6, field_id: 12, index: 1, comparator: 'equals', value: 'service', view_id: 6 },
// { id: 7, field_id: 12, index: 1, comparator: 'equals', value: 'inventory', view_id: 7 },
// { id: 8, field_id: 12, index: 1, comparator: 'equals', value: 'non-inventory', view_id: 8 },
// // Manual Journals.
// { id: 9, field_id: 26, index: 1, comparator: 'equals', value: 'Journal', view_id: 9 },
// { id: 10, field_id: 26, index: 1, comparator: 'equals', value: 'CreditNote', view_id: 10 },
// { id: 11, field_id: 26, index: 1, comparator: 'equals', value: 'Reconciliation', view_id: 11 },
// Expenses
// { id: 12, field_id: index: }
]);
});
};
exports.down = (knex) => {
}

View File

@@ -1,177 +0,0 @@
exports.seed = (knex) => {
console.log(knex.tenantId);
// Deletes ALL existing entries
return knex('accounts').del()
.then(() => {
// Inserts seed entries
return knex('accounts').insert([
{
id: 1,
name: 'Petty Cash',
slug: 'petty-cash',
account_type_id: 2,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 2,
name: 'Bank',
slug: 'bank',
account_type_id: 2,
parent_account_id: null,
code: '2000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 3,
name: 'Other Income',
slug: 'other-income',
account_type_id: 7,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 4,
name: 'Interest Income',
slug: 'interest-income',
account_type_id: 7,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 5,
name: 'Opening Balance',
slug: 'opening-balance',
account_type_id: 5,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 6,
name: 'Depreciation Expense',
slug: 'depreciation-expense',
account_type_id: 6,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 7,
name: 'Interest Expense',
slug: 'interest-expense',
account_type_id: 6,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 8,
name: 'Payroll Expenses',
slug: 'payroll-expenses',
account_type_id: 6,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 9,
name: 'Other Expenses',
slug: 'other-expenses',
account_type_id: 6,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 10,
name: 'Accounts Receivable',
slug: 'accounts-receivable',
account_type_id: 8,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 11,
name: 'Accounts Payable',
slug: 'accounts-payable',
account_type_id: 9,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 12,
name: 'Cost of Goods Sold (COGS)',
slug: 'cost-of-goods-sold',
account_type_id: 12,
predefined: 1,
parent_account_id: null,
index: 1,
active: 1,
description: 1,
},
{
id: 13,
name: 'Inventory Asset',
slug: 'inventory-asset',
account_type_id: 14,
predefined: 1,
parent_account_id: null,
index: 1,
active: 1,
description: '',
},
{
id: 14,
name: 'Sales of Product Income',
slug: 'sales-of-product-income',
account_type_id: 7,
predefined: 1,
parent_account_id: null,
index: 1,
active: 1,
description: '',
}
]);
});
};

View File

@@ -1,67 +0,0 @@
exports.seed = function (knex) {
// Deletes ALL existing entries
return knex('resource_fields')
.del()
.then(() => {
// Inserts seed entries
return knex('resource_fields').insert([
{
id: 1,
label_name: 'Name',
key: 'name',
data_type: '',
active: 1,
predefined: 1,
},
{
id: 2,
label_name: 'Code',
key: 'code',
data_type: '',
active: 1,
predefined: 1,
},
{
id: 3,
label_name: 'Account Type',
key: 'type',
data_type: '',
active: 1,
predefined: 1,
data_resource_id: 8,
},
{
id: 4,
label_name: 'Description',
key: 'description',
data_type: '',
active: 1,
predefined: 1,
},
{
id: 5,
label_name: 'Account Normal',
key: 'normal',
data_type: 'string',
active: 1,
predefined: 1,
},
{
id: 6,
label_name: 'Root Account Type',
key: 'root_type',
data_type: 'string',
active: 1,
predefined: 1,
},
{
id: 7,
label_name: 'Active',
key: 'active',
data_type: 'boolean',
active: 1,
predefined: 1,
},
]);
});
};

View File

@@ -1,25 +0,0 @@
exports.seed = (knex) => {
// Deletes ALL existing entries
return knex('resources').del()
.then(() => {
// Inserts seed entries
return knex('resources').insert([
{ id: 1, name: 'accounts' },
{ id: 8, name: 'accounts_types' },
{ id: 2, name: 'items' },
{ id: 3, name: 'expenses' },
{ id: 4, name: 'manual_journals' },
{ id: 5, name: 'items_categories' },
{ id: 6, name: 'customers' },
{ id: 7, name: 'vendors' },
{ id: 9, name: 'sales_estimates' },
{ id: 10, name: 'sales_receipts' },
{ id: 11, name: 'sales_invoices' },
{ id: 12, name: 'sales_payment_receives' },
{ id: 13, name: 'bills' },
{ id: 14, name: 'bill_payments' },
{ id: 16, name: 'payment_receives' },
]);
});
};

View File

@@ -1,424 +0,0 @@
exports.seed = (knex) => {
return knex('resource_fields').del()
.then(() => {
return knex('resource_fields').insert([
// Accounts
{
id: 1,
resource_id: 1,
label_name: 'Account Name',
key: 'name',
data_type: 'textbox',
predefined: 1,
columnable: true,
},
{
id: 2,
resource_id: 1,
label_name: 'Code',
key: 'code',
data_type: 'textbox',
predefined: 1,
columnable: true,
},
{
id: 3,
resource_id: 1,
label_name: 'Type',
key: 'type',
data_type: 'options',
predefined: 1,
columnable: true,
data_resource: 'accounts_types',
},
{
id: 5,
resource_id: 1,
label_name: 'Description',
data_type: 'textarea',
key: 'description',
predefined: 1,
columnable: true,
},
{
id: 6,
resource_id: 1,
label_name: 'Root type',
data_type: 'textbox',
key: 'root_type',
predefined: 1,
columnable: true,
},
{
id: 16,
resource_id: 1,
label_name: 'Created at',
data_type: 'date',
key: 'created_at',
predefined: 1,
columnable: true,
},
{
id: 17,
resource_id: 1,
data_type: 'boolean',
label_name: 'Active',
key: 'active',
predefined: 1,
columnable: true,
},
{
id: 31,
resource_id: 1,
data_type: 'numeric',
label_name: 'Balance',
key: 'balance',
predefined: 1,
columnable: true,
},
{
id: 32,
resource_id: 1,
data_type: 'options',
label_name: 'Currency',
key: 'currency',
predefined: 1,
columnable: true,
data_resource: 'currencies',
},
{
id: 33,
resource_id: 1,
data_type: 'options',
label_name: 'Normal',
key: 'normal',
predefined: 1,
columnable: true,
options: JSON.stringify([
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
])
},
// Manual Journals
{
id: 18,
resource_id: 4,
data_type: 'date',
label_name: 'Date',
key: 'date',
predefined: 1,
columnable: true,
},
{
id: 19,
resource_id: 4,
data_type: 'date',
label_name: 'Created At',
key: 'created_at',
predefined: 1,
columnable: true,
},
{
id: 20,
resource_id: 4,
data_type: 'textbox',
label_name: 'Journal Number',
key: 'journal_number',
predefined: 1,
columnable: true,
},
{
id: 21,
resource_id: 4,
data_type: 'boolean',
label_name: 'Active',
key: 'status',
predefined: 1,
columnable: true,
},
{
id: 22,
resource_id: 4,
data_type: 'textbox',
label_name: 'Reference',
key: 'reference',
predefined: 1,
columnable: true,
},
{
id: 23,
resource_id: 4,
data_type: 'textbox',
label_name: 'Description',
key: 'description',
predefined: 1,
columnable: true,
},
{
id: 24,
resource_id: 4,
data_type: 'numeric',
label_name: 'Amount',
key: 'amount',
predefined: 1,
columnable: true,
},
{
id: 25,
resource_id: 4,
data_type: 'optons',
label_name: 'User',
key: 'user',
predefined: 1,
columnable: true,
data_resource: 'users',
},
{
id: 26,
resource_id: 4,
data_type: 'textbox',
label_name: 'Journal Type',
key: 'journal_type',
predefined: 1,
columnable: true,
},
// Expenses
{
id: 7,
resource_id: 3,
label_name: 'Payment Date',
key: 'payment_date',
data_type: 'date',
predefined: 1,
columnable: true,
},
{
id: 9,
resource_id: 3,
key: 'payment_account',
label_name: 'Payment Account',
data_type: 'options',
predefined: 1,
columnable: true,
data_resource: 'accounts',
},
{
id: 10,
resource_id: 3,
key: 'total_amount',
label_name: 'Amount',
data_type: 'numeric',
predefined: 1,
columnable: true,
},
{
id: 27,
resource_id: 3,
label_name: 'Reference No.',
key: 'reference_no',
data_type: 'textbox',
predefined: 1,
columnable: true,
},
{
id: 28,
resource_id: 3,
key: 'description',
label_name: 'Description',
data_type: 'textbox',
predefined: 1,
columnable: true,
},
{
id: 29,
resource_id: 3,
key: 'published',
label_name: 'Published',
data_type: 'checkbox',
predefined: 1,
columnable: true,
},
{
id: 30,
resource_id: 3,
key: 'user',
data_type: 'options',
label_name: 'User',
predefined: 1,
columnable: true,
data_resource: 'users',
},
// Items
{
id: 11,
resource_id: 2,
label_name: 'Name',
key: 'name',
data_type: 'textbox',
predefined: 1,
columnable: true,
},
{
id: 12,
resource_id: 2,
label_name: 'Type',
key: 'type',
data_type: 'textbox',
predefined: 1,
columnable: true,
},
// Sales Estimates
{
label_name: 'Customer name',
key: 'customer_name',
},
{
label_name: 'Amount',
key: 'amount',
},
{
label_name: 'Estimate number',
key: 'estimate_number',
},
{
label_name: 'Estimate date',
key: 'estimate_date',
},
{
label_name: 'Expiration date',
key: 'expiration_date',
},
{
label_name: 'Reference',
key: 'reference',
},
{
label_name: 'Terms and conditions',
key: 'terms_conditions',
},
{
label_name: 'Note',
key: 'note',
},
// Sales invoices
// {
// label_name: 'Customer name',
// ley: 'customer_name',
// },
// {
// label_name: 'Amount',
// ley: 'amount',
// },
// {
// label_name: 'Invoice number',
// ley: 'invoice_no',
// },
// {
// label_name: 'Invoice date',
// ley: 'invoice_date',
// },
// {
// label_name: 'Reference',
// ley: 'reference',
// },
// {
// label_name: 'Payment amount',
// ley: 'payment_amount',
// },
// {
// label_name: 'Invoice message',
// ley: 'invoice_no',
// },
// {
// label_name: 'Terms and conditions',
// key: 'terms_conditions',
// },
// // Sales receipts
// {
// label_name: 'Deposit account',
// key: 'deposit_account',
// },
// {
// label_name: 'Customer name',
// key: 'customer_name',
// },
// {
// label_name: 'Receipt date',
// key: 'receipt_date',
// },
// {
// label_name: 'Reference No',
// key: 'reference',
// },
// {
// label_name: 'Receipt message',
// key: 'receipt_message',
// },
// {
// label_name: 'Sent to email',
// key: 'email_send_to',
// },
// // Payment Receives
// {
// label_name: 'Customer name',
// key: 'customer_name',
// },
// {
// label_name: 'Payment date',
// key: 'payment_date',
// },
// {
// label_name: 'Amount',
// key: 'amount',
// },
// {
// label_name: 'Reference No',
// key: 'reference',
// },
// {
// label_name: 'Deposit account',
// key: 'deposit_account',
// },
// {
// label_name: 'Payment receive no.',
// key: 'payment_receive_no',
// },
// // Purchases bills.
// {
// label_name: 'Bill number',
// key: 'bill_number'
// },
// {
// label_name: 'Bill date',
// key: 'bill_date'
// },
// {
// label_name: 'Amount',
// key: 'amount'
// },
// {
// label_name: 'Vendor name',
// key: 'vendor_name'
// },
// {
// label_name: 'Due date',
// key: 'due_date'
// },
// {
// label_name: 'Note',
// key: 'note'
// },
]);
});
};

View File

@@ -1,18 +0,0 @@
exports.seed = (knex) => {
return knex('users').del()
.then(() => {
return knex('users').insert([
{
first_name: 'Ahmed',
last_name: 'Mohamed',
email: 'admin@admin.com',
phone_number: '0920000000',
password: '$2b$10$LGSMrezP8IHBb/cNMlc1ZOKA59Fc9rY0IEk2u.iuF/y6yS2YlGP7i', // test
active: 1,
language: 'ar',
created_at: new Date(),
},
]);
});
};

View File

@@ -1,32 +0,0 @@
exports.seed = (knex) => {
// Deletes ALL existing entries
return knex('views').del()
.then(() => {
// Inserts seed entries
return knex('views').insert([
// Accounts
{ id: 15, name: 'Inactive', roles_logic_expression: '1', resource_id: 1, predefined: true },
{ id: 1, name: 'Assets', roles_logic_expression: '1', resource_id: 1, predefined: true },
{ id: 2, name: 'Liabilities', roles_logic_expression: '1', resource_id: 1, predefined: true },
{ id: 3, name: 'Equity', roles_logic_expression: '1', resource_id: 1, predefined: true },
{ id: 4, name: 'Income', roles_logic_expression: '1', resource_id: 1, predefined: true },
{ id: 5, name: 'Expenses', roles_logic_expression: '1', resource_id: 1, predefined: true },
// Items
{ id: 6, name: 'Services', roles_logic_expression: '1', resource_id: 2, predefined: true },
{ id: 7, name: 'Inventory', roles_logic_expression: '1', resource_id: 2, predefined: true },
{ id: 8, name: 'Non-Inventory', roles_logic_expression: '1', resource_id: 2, predefined: true },
// Manual Journals
{ id: 9, name: 'Journal', roles_logic_expression: '1', resource_id: 4, predefined: true },
{ id: 10, name: 'Credit', roles_logic_expression: '1', resource_id: 4, predefined: true },
{ id: 11, name: 'Reconciliation', roles_logic_expression: '1', resource_id: 4, predefined: true },
// Expenses
{ id: 12, name: 'Interest', roles_logic_expression: '1', resource_id: 3, predefined: false, },
{ id: 13, name: 'Depreciation', roles_logic_expression: '1', resource_id: 3, predefined: false, },
{ id: 14, name: 'Payroll', roles_logic_expression: '1', resource_id: '3', predefined: false },
]);
});
};

View File

@@ -1,30 +0,0 @@
exports.seed = (knex) => {
// Deletes ALL existing entries
return knex('view_roles').del()
.then(() => {
// Inserts seed entries
return knex('view_roles').insert([
// Accounts
{ id: 1, field_id: 6, index: 1, comparator: 'equals', value: 'asset', view_id: 1 },
{ id: 2, field_id: 6, index: 1, comparator: 'equals', value: 'liability', view_id: 2 },
{ id: 3, field_id: 6, index: 1, comparator: 'equals', value: 'equity', view_id: 3 },
{ id: 4, field_id: 6, index: 1, comparator: 'equals', value: 'income', view_id: 4 },
{ id: 5, field_id: 6, index: 1, comparator: 'equals', value: 'expense', view_id: 5 },
{ id: 12, field_id: 17, index: 1, comparator: 'is', value: 1, view_id: 15 },
// Items.
{ id: 6, field_id: 12, index: 1, comparator: 'equals', value: 'service', view_id: 6 },
{ id: 7, field_id: 12, index: 1, comparator: 'equals', value: 'inventory', view_id: 7 },
{ id: 8, field_id: 12, index: 1, comparator: 'equals', value: 'non-inventory', view_id: 8 },
// Manual Journals.
{ id: 9, field_id: 26, index: 1, comparator: 'equals', value: 'Journal', view_id: 9 },
{ id: 10, field_id: 26, index: 1, comparator: 'equals', value: 'CreditNote', view_id: 10 },
{ id: 11, field_id: 26, index: 1, comparator: 'equals', value: 'Reconciliation', view_id: 11 },
// Expenses
// { id: 12, field_id: index: }
]);
});
};

View File

@@ -1,4 +1,4 @@
import { IDynamicListFilterDTO } from 'interfaces/DynamicFilter';
export interface IAccountDTO {
name: string,
@@ -12,4 +12,8 @@ export interface IAccount {
code: string,
description: string,
accountTypeNumber: number,
};
};
export interface IAccountsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string,
};

View File

@@ -1,5 +1,8 @@
// Contact Interfaces.
import { IDynamicListFilter } from "./DynamicFilter";
// ----------------------------------
export interface IContactAddress {
billingAddress1: string,
@@ -167,4 +170,16 @@ export interface IVendorEditDTO extends IContactAddressDTO {
note?: string,
active?: boolean,
};
export interface IVendorsFilter extends IDynamicListFilter {
stringifiedFilterRoles?: string,
page?: number,
pageSize?: number,
};
export interface ICustomerFilter extends IDynamicListFilter {
stringifiedFilterRoles?: string,
page?: number,
pageSize?: number,
};

View File

@@ -0,0 +1,20 @@
export interface IDynamicFilter {
setTableName(tableName: string): void;
buildQuery(): void;
}
export interface IFilterRole {
fieldKey: string,
value: string,
condition?: string,
index?: number,
comparator?: string,
};
export interface IDynamicListFilterDTO {
customViewId?: number,
filterRoles?: IFilterRole[],
columnSortBy: string,
sortOrder: string,
}

View File

@@ -1,7 +1,17 @@
import { IDynamicListFilter } from 'interfaces/DynamicFilter';
export interface IItem{
id: number,
name: string,
type: string,
}
}
export interface IItemsService {
}
export interface IItemsFilter extends IDynamicListFilter {
stringifiedFilterRoles?: string,
page?: number,
pageSize?: number,
};

View File

@@ -0,0 +1,22 @@
export interface IResource {
id: number,
key: string,
}
export interface IResourceField {
labelName: string,
key: string,
dataType: string,
helpText?: string | null,
default?: string,
predefined: boolean,
active: boolean,
builtin: boolean,
columnable: boolean,
index: number,
dataResource: string,
resourceId: number,
options: any;
}

View File

@@ -0,0 +1,24 @@
export interface IView {
id: number,
name: string,
predefined: boolean,
resourceId: number,
favourite: boolean,
rolesLogicRxpression: string,
};
export interface IViewRole {
id: number,
fieldId: number,
index: number,
comparator: string,
value: string,
viewId: number,
};
export interface IViewHasColumn {
viewId: number,
fieldId: number,
index: number,
}

View File

@@ -1,153 +1,22 @@
import { IInventoryTransaction, IInventoryLotCost } from './InventoryTransaction';
import {
IBillPaymentEntry,
IBillPayment,
IBillPaymentOTD,
} from './BillPayment';
import { IInventoryCostMethod } from './InventoryCostMethod';
import { IItemEntry } from './ItemEntry';
import { IItem } from './Item';
import { ILicense, ILicensesFilter } from './License';
import { IItemCategory, IItemCategoryOTD } from './ItemCategory';
import {
IPaymentModel,
ILicensePaymentModel,
IPaymentMethod,
ILicensePaymentMethod,
IPaymentContext,
} from './Payment';
import {
ISaleInvoice,
ISaleInvoiceOTD,
} from './SaleInvoice';
import {
IPaymentReceive,
IPaymentReceiveOTD,
} from './PaymentReceive';
import {
ISaleEstimate,
ISaleEstimateOTD,
} from './SaleEstimate';
import {
IRegisterDTO,
} from './Register';
import {
ISystemUser,
ISystemUserDTO,
IInviteUserInput,
} from './User';
import {
IMetadata,
IMetaQuery,
IMetableStore,
IMetableStoreStorage,
} from './Metable';
import {
IOptionDTO,
IOptionsDTO,
} from './Options';
import {
IAccount,
IAccountDTO,
} from './Account';
import {
IJournalEntry,
IJournalPoster,
TEntryType,
IAccountChange,
IAccountsChange,
} from './Journal';
import {
IContactAddress,
IContact,
IContactNewDTO,
IContactEditDTO,
ICustomer,
ICustomerNewDTO,
ICustomerEditDTO,
} from './Contact';
import {
IExpense,
IExpenseCategory,
IExpenseDTO,
IExpenseCategoryDTO,
IExpensesService,
} from './Expenses';
import {
ITenant,
ITenantDBManager,
ITenantManager,
ISystemService,
} from './Tenancy';
export {
IAccount,
IAccountDTO,
IBillPaymentEntry,
IBillPayment,
IBillPaymentOTD,
IInventoryTransaction,
IInventoryLotCost,
IInventoryCostMethod,
IItemEntry,
IItem,
ILicense,
ILicensesFilter,
IItemCategory,
IItemCategoryOTD,
IPaymentModel,
IPaymentMethod,
IPaymentContext,
ILicensePaymentModel,
ILicensePaymentMethod,
ISaleInvoice,
ISaleInvoiceOTD,
ISaleEstimate,
ISaleEstimateOTD,
IPaymentReceive,
IPaymentReceiveOTD,
IRegisterDTO,
ISystemUser,
ISystemUserDTO,
IInviteUserInput,
IMetadata,
IMetaQuery,
IMetableStore,
IMetableStoreStorage,
IOptionDTO,
IOptionsDTO,
IJournalEntry,
IJournalPoster,
TEntryType,
IAccountChange,
IAccountsChange,
IContactAddress,
IContact,
IContactNewDTO,
IContactEditDTO,
ICustomer,
ICustomerNewDTO,
ICustomerEditDTO,
IExpense,
IExpenseCategory,
IExpenseDTO,
IExpenseCategoryDTO,
IExpensesService,
ITenant,
ITenantDBManager,
ITenantManager,
ISystemService,
};
export * from './InventoryTransaction';
export * from './BillPayment';
export * from './InventoryCostMethod';
export * from './ItemEntry';
export * from './Item';
export * from './License';
export * from './ItemCategory';
export * from './Payment';
export * from './SaleInvoice';
export * from './PaymentReceive';
export * from './SaleEstimate';
export * from './Register';
export * from './User';
export * from './Metable';
export * from './Options';
export * from './Account';
export * from './DynamicFilter';
export * from './Journal';
export * from './Contact';
export * from './Expenses';
export * from './Tenancy';
export * from './View';

View File

@@ -32,7 +32,6 @@ export default class ComputeItemCostJob {
Logger.info(`Compute item cost - completed: ${job.attrs.data}`);
done();
} catch(e) {
console.log(e);
Logger.info(`Compute item cost: ${job.attrs.data}, error: ${e}`);
done(e);
}

View File

@@ -14,7 +14,6 @@ export default class SendLicenseViaEmailJob {
Logger.debug(`Send license via email - completed: ${job.attrs.data}`);
done();
} catch(e) {
console.log(e);
Logger.error(`Send license via email: ${job.attrs.data}, error: ${e}`);
done(e);
}

View File

@@ -15,7 +15,6 @@ export default class SendLicenseViaPhoneJob {
Logger.debug(`Send license via phone number - completed: ${job.attrs.data}`);
done();
} catch(e) {
console.log(e);
Logger.error(`Send license via phone number: ${job.attrs.data}, error: ${e}`);
done(e);
}

View File

@@ -14,7 +14,6 @@ export default class WriteInvoicesJournalEntries {
Logger.info(`Write sales invoices journal entries - completed: ${job.attrs.data}`);
done();
} catch(e) {
console.log(e);
Logger.info(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`);
done(e);
}

View File

@@ -3,6 +3,7 @@ import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbst
import {
buildFilterQuery,
} from 'lib/ViewRolesBuilder';
import { IFilterRole } from 'interfaces';
export default class FilterRoles extends DynamicFilterRoleAbstructor {
/**
@@ -10,31 +11,17 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
* @param {Array} filterRoles -
* @param {Array} resourceFields -
*/
constructor(filterRoles, resourceFields) {
constructor(filterRoles: IFilterRole[]) {
super();
this.filterRoles = filterRoles.map((role, index) => ({
...role,
index: index + 1,
columnKey: role.field_key,
condition: role.comparator === 'AND' ? '&&' : '||',
}));
this.resourceFields = resourceFields;
this.filterRoles = filterRoles;
}
validateFilterRoles() {
const filterFieldsKeys = this.filterRoles.map((r) => r.field_key);
const resourceFieldsKeys = this.resourceFields.map((r) => r.key);
return difference(filterFieldsKeys, resourceFieldsKeys);
}
// @private
buildLogicExpression() {
private buildLogicExpression(): string {
let expression = '';
this.filterRoles.forEach((role, index) => {
expression += (index === 0)
? `${role.index} ` : `${role.condition} ${role.index} `;
expression += (index === 0) ?
`${role.index} ` :
`${role.condition} ${role.index} `;
});
return expression.trim();
}

View File

@@ -1,29 +0,0 @@
export default class DynamicFilterAbstructor {
constructor() {
this.filterRoles = [];
this.tableName = '';
}
setTableName(tableName) {
this.tableName = tableName;
}
/**
* @interface
*/
// eslint-disable-next-line class-methods-use-this
buildLogicExpression() {}
/**
* @interface
*/
// eslint-disable-next-line class-methods-use-this
validateFilterRoles() {}
/**
* @interface
*/
// eslint-disable-next-line class-methods-use-this
buildQuery() {}
}

View File

@@ -0,0 +1,10 @@
import { IFilterRole, IDynamicFilter } from "interfaces";
export default class DynamicFilterAbstructor implements IDynamicFilter {
filterRoles: IFilterRole[] = [];
tableName: string;
setTableName(tableName) {
this.tableName = tableName;
}
}

View File

@@ -1,32 +0,0 @@
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import { getRoleFieldColumn } from 'lib/ViewRolesBuilder';
export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
constructor(sortByFieldKey, sortDirection) {
super();
this.filterRoles = {
columnKey: sortByFieldKey,
value: sortDirection,
comparator: 'sort_by',
};
}
/**
* Builds database query of sort by column on the given direction.
*/
buildQuery() {
const { columnKey = null, value = null } = this.filterRoles;
return (builder) => {
const fieldRelation = getRoleFieldColumn(this.tableName, columnKey);
const comparatorColumn =
fieldRelation.relationColumn ||
`${this.tableName}.${fieldRelation.column}`;
if (columnKey) {
builder.orderBy(`${comparatorColumn}`, value.toLowerCase());
}
};
}
}

View File

@@ -0,0 +1,40 @@
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import { getRoleFieldColumn, validateFieldKeyExistance } from 'lib/ViewRolesBuilder';
export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
sortRole: { fieldKey: string, order: string } = {};
/**
* Constructor method.
* @param {string} sortByFieldKey
* @param {string} sortDirection
*/
constructor(sortByFieldKey: string, sortDirection: string) {
super();
this.sortRole = {
fieldKey: sortByFieldKey,
order: sortDirection,
};
}
validate() {
validateFieldKeyExistance(this.tableName, this.sortRole.fieldKey);
}
/**
* Builds database query of sort by column on the given direction.
*/
buildQuery() {
return (builder) => {
const fieldRelation = getRoleFieldColumn(this.tableName, this.sortRole.fieldKey);
const comparatorColumn =
fieldRelation.relationColumn ||
`${this.tableName}.${fieldRelation.column}`;
if (this.sortRole.fieldKey) {
builder.orderBy(`${comparatorColumn}`, this.sortRole.order);
}
};
}
}

View File

@@ -1,3 +1,4 @@
import { IFilterRole } from 'interfaces';
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import {
validateViewRoles,
@@ -5,20 +6,20 @@ import {
} from 'lib/ViewRolesBuilder';
export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
logicExpression: string;
/**
* Constructor method.
* @param {*} filterRoles -
* @param {*} logicExpression -
* @param {*} filterRoles - Filter roles.
* @param {*} logicExpression - Logic expression.
*/
constructor(filterRoles, logicExpression) {
constructor(filterRoles: IFilterRole[], logicExpression: string) {
super();
this.filterRoles = filterRoles;
this.logicExpression = logicExpression
.replace('AND', '&&')
.replace('OR', '||');
this.tableName = '';
}
/**
@@ -31,7 +32,7 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
/**
* Validates filter roles.
*/
validateFilterRoles() {
validate() {
return validateViewRoles(this.filterRoles, this.logicExpression);
}

View File

@@ -1,20 +1,46 @@
import { difference } from 'lodash';
import { difference, filter } from 'lodash';
import moment from 'moment';
import { Lexer } from 'lib/LogicEvaluation/Lexer';
import Parser from 'lib/LogicEvaluation/Parser';
import QueryParser from 'lib/LogicEvaluation/QueryParser';
import resourceFieldsKeys from 'data/ResourceFieldsKeys';
import { IFilterRole } from 'interfaces';
// const role = {
// compatotor: String,
// value: String,
// columnKey: String,
// columnSlug: String,
// index: Number,
// }
const numberRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
switch (role.comparator) {
case 'equals':
case 'equal':
default:
return (builder) => {
builder.where(comparatorColumn, '=', role.value);
};
case 'not_equals':
case 'not_equal':
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case 'bigger_than':
case 'bigger':
return (builder) => {
builder.where(comparatorColumn, '>', role.value);
};
case 'bigger_or_equals':
return (builder) => {
builder.where(comparatorColumn, '>=', role.value);
};
case 'smaller_than':
case 'smaller':
return (builder) => {
builder.where(comparatorColumn, '<', role.value);
};
case 'smaller_or_equals':
return (builder) => {
builder.where(comparatorColumn, '<=', role.value);
};
}
};
const textRoleQueryBuilder = (role, comparatorColumn) => {
const textRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
switch (role.comparator) {
case 'equals':
default:
@@ -39,7 +65,7 @@ const textRoleQueryBuilder = (role, comparatorColumn) => {
}
};
const dateQueryBuilder = (role, comparatorColumn) => {
const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
switch(role.comparator) {
case 'after':
case 'before':
@@ -81,11 +107,11 @@ const dateQueryBuilder = (role, comparatorColumn) => {
/**
* Get field column metadata and its relation with other tables.
* @param {String} tableName - Table name of target column.
* @param {String} columnKey - Target column key that stored in resource field.
* @param {String} fieldKey - Target column key that stored in resource field.
*/
export function getRoleFieldColumn(tableName, columnKey) {
export function getRoleFieldColumn(tableName: string, fieldKey: string) {
const tableFields = resourceFieldsKeys[tableName];
return (tableFields[columnKey]) ? tableFields[columnKey] : null;
return (tableFields[fieldKey]) ? tableFields[fieldKey] : null;
}
/**
@@ -93,13 +119,15 @@ export function getRoleFieldColumn(tableName, columnKey) {
* @param {String} tableName -
* @param {Object} role -
*/
export function buildRoleQuery(tableName, role) {
const fieldRelation = getRoleFieldColumn(tableName, role.columnKey);
export function buildRoleQuery(tableName: string, role: IFilterRole) {
const fieldRelation = getRoleFieldColumn(tableName, role.fieldKey);
const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`;
switch (fieldRelation.columnType) {
case 'number':
return numberRoleQueryBuilder(role, comparatorColumn);
case 'date':
return dateQueryBuilder(role, comparatorColumn);
return dateQueryBuilder(role, comparatorColumn);
case 'text':
case 'varchar':
default:
@@ -112,7 +140,7 @@ export function buildRoleQuery(tableName, role) {
* @param {String} column -
* @return {String} - join relation table.
*/
export const getTableFromRelationColumn = (column) => {
export const getTableFromRelationColumn = (column: string) => {
const splitedColumn = column.split('.');
return (splitedColumn.length > 0) ? splitedColumn[0] : '';
};
@@ -122,10 +150,10 @@ export const getTableFromRelationColumn = (column) => {
* @param {String} tableName -
* @param {Array} roles -
*/
export function buildFilterRolesJoins(tableName, roles) {
export function buildFilterRolesJoins(tableName: string, roles: IFilterRole[]) {
return (builder) => {
roles.forEach((role) => {
const fieldColumn = getRoleFieldColumn(tableName, role.columnKey);
const fieldColumn = getRoleFieldColumn(tableName, role.fieldKey);
if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation);
@@ -135,7 +163,7 @@ export function buildFilterRolesJoins(tableName, roles) {
};
}
export function buildSortColumnJoin(tableName, sortColumnKey) {
export function buildSortColumnJoin(tableName: string, sortColumnKey: string) {
return (builder) => {
const fieldColumn = getRoleFieldColumn(tableName, sortColumnKey);
@@ -152,7 +180,7 @@ export function buildSortColumnJoin(tableName, sortColumnKey) {
* @param {Array} roles -
* @return {Function}
*/
export function buildFilterRolesQuery(tableName, roles, logicExpression = '') {
export function buildFilterRolesQuery(tableName: string, roles: IFilterRole[], logicExpression: string = '') {
const rolesIndexSet = {};
roles.forEach((role) => {
@@ -176,32 +204,12 @@ export function buildFilterRolesQuery(tableName, roles, logicExpression = '') {
* @param {Array} roles -
* @param {String} logicExpression -
*/
export const buildFilterQuery = (tableName, roles, logicExpression) => {
export const buildFilterQuery = (tableName: string, roles, logicExpression: string) => {
return (builder) => {
buildFilterRolesQuery(tableName, roles, logicExpression)(builder);
};
};
/**
* Validates the view logic expression.
* @param {String} logicExpression -
* @param {Array} indexes -
*/
export function validateFilterLogicExpression(logicExpression, indexes) {
const logicExpIndexes = logicExpression.match(/\d+/g) || [];
return !difference(logicExpIndexes.map(Number), indexes).length;
}
/**
* Validates view roles.
* @param {Array} roles -
* @param {String} logicExpression -
* @return {Boolean}
*/
export function validateViewRoles(roles, logicExpression) {
return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index));
}
/**
* Mapes the view roles to view conditionals.
* @param {Array} viewRoles -
@@ -211,9 +219,10 @@ export function mapViewRolesToConditionals(viewRoles) {
return viewRoles.map((viewRole) => ({
comparator: viewRole.comparator,
value: viewRole.value,
index: viewRole.index,
columnKey: viewRole.field.key,
slug: viewRole.field.slug,
index: viewRole.index,
}));
}
@@ -231,7 +240,7 @@ export function mapFilterRolesToDynamicFilter(roles) {
* @param {String} columnKey -
* @param {String} sortDirection -
*/
export function buildSortColumnQuery(tableName, columnKey, sortDirection) {
export function buildSortColumnQuery(tableName: string, columnKey: string, sortDirection: string) {
const fieldRelation = getRoleFieldColumn(tableName, columnKey);
const sortColumn = fieldRelation.relation || `${tableName}.${fieldRelation.column}`;
@@ -239,4 +248,27 @@ export function buildSortColumnQuery(tableName, columnKey, sortDirection) {
builder.orderBy(sortColumn, sortDirection);
buildSortColumnJoin(tableName, columnKey)(builder);
};
}
export function validateFilterLogicExpression(logicExpression: string, indexes) {
const logicExpIndexes = logicExpression.match(/\d+/g) || [];
const diff = !difference(logicExpIndexes.map(Number), indexes).length;
}
export function validateRolesLogicExpression(logicExpression: string, roles: IFilterRole[]) {
return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index));
}
export function validateFieldKeyExistance(tableName: string, fieldKey: string) {
if (!resourceFieldsKeys?.[tableName]?.[fieldKey])
return fieldKey;
else
return false;
}
export function validateFilterRolesFieldsExistance(tableName, filterRoles: IFilterRole[]) {
return filterRoles.filter((filterRole: IFilterRole) => {
return validateFieldKeyExistance(tableName, filterRole.fieldKey);
});
}

View File

@@ -28,11 +28,8 @@ import SaleEstimateEntry from 'models/SaleEstimateEntry';
import PaymentReceive from 'models/PaymentReceive';
import PaymentReceiveEntry from 'models/PaymentReceiveEntry';
import Option from 'models/Option';
import Resource from 'models/Resource';
import InventoryCostLotTracker from 'models/InventoryCostLotTracker';
import InventoryTransaction from 'models/InventoryTransaction';
import ResourceField from 'models/ResourceField';
import ResourceFieldMetadata from 'models/ResourceFieldMetadata';
import ManualJournal from 'models/ManualJournal';
import Media from 'models/Media';
import MediaLink from 'models/MediaLink';
@@ -67,11 +64,8 @@ export default (knex) => {
SaleEstimateEntry,
PaymentReceive,
PaymentReceiveEntry,
Resource,
InventoryTransaction,
InventoryCostLotTracker,
ResourceField,
ResourceFieldMetadata,
Media,
MediaLink,
Contact,

View File

@@ -3,6 +3,8 @@ import AccountTypeRepository from 'repositories/AccountTypeRepository';
import VendorRepository from 'repositories/VendorRepository';
import CustomerRepository from 'repositories/CustomerRepository';
import ExpenseRepository from 'repositories/ExpenseRepository';
import ViewRepository from 'repositories/ViewRepository';
import ViewRoleRepository from 'repositories/ViewRoleRepository';
export default (tenantId: number) => {
return {
@@ -11,5 +13,7 @@ export default (tenantId: number) => {
customerRepository: new CustomerRepository(tenantId),
vendorRepository: new VendorRepository(tenantId),
expenseRepository: new ExpenseRepository(tenantId),
viewRepository: new ViewRepository(tenantId),
viewRoleRepository: new ViewRoleRepository(tenantId),
};
};

View File

@@ -1,52 +0,0 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Resource extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'resources';
}
/**
* Timestamp columns.
*/
static get hasTimestamps() {
return false;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const View = require('models/View');
const ResourceField = require('models/ResourceField');
return {
/**
* Resource model may has many views.
*/
views: {
relation: Model.HasManyRelation,
modelClass: this.relationBindKnex(View.default),
join: {
from: 'resources.id',
to: 'views.resourceId',
},
},
/**
* Resource model may has many fields.
*/
fields: {
relation: Model.HasManyRelation,
modelClass: this.relationBindKnex(ResourceField.default),
join: {
from: 'resources.id',
to: 'resource_fields.resourceId',
},
},
};
}
}

View File

@@ -1,70 +0,0 @@
import { snakeCase } from 'lodash';
import { Model } from 'objection';
import path from 'path';
import TenantModel from 'models/TenantModel';
export default class ResourceField extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'resource_fields';
}
static get jsonAttributes() {
return ['options'];
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
whereNotPredefined(query) {
query.whereNot('predefined', true);
},
};
}
/**
* Timestamp columns.
*/
static get hasTimestamps() {
return false;
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['key'];
}
/**
* Resource field key.
*/
key() {
return snakeCase(this.labelName);
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Resource = require('models/Resource');
return {
/**
* Resource field may belongs to resource model.
*/
resource: {
relation: Model.BelongsToOneRelation,
modelClass: this.relationBindKnex(Resource.default),
join: {
from: 'resource_fields.resourceId',
to: 'resources.id',
},
},
};
}
}

View File

@@ -1,10 +0,0 @@
import TenantModel from 'models/TenantModel';
export default class ResourceFieldMetadata extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'resource_custom_fields_metadata';
}
}

View File

@@ -12,7 +12,7 @@ export default class View extends TenantModel {
/**
* Model timestamps.
*/
static get timestamps() {
get timestamps() {
return ['createdAt', 'updatedAt'];
}
@@ -40,23 +40,10 @@ export default class View extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
const Resource = require('models/Resource');
const ViewColumn = require('models/ViewColumn');
const ViewRole = require('models/ViewRole');
return {
/**
* View model belongs to resource model.
*/
resource: {
relation: Model.BelongsToOneRelation,
modelClass: this.relationBindKnex(Resource.default),
join: {
from: 'views.resourceId',
to: 'resources.id',
},
},
/**
* View model may has many columns.
*/

View File

@@ -13,20 +13,9 @@ export default class ViewColumn extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
const ResourceField = require('models/ResourceField');
return {
/**
* View role model may belongs to resource field model.
*/
field: {
relation: Model.BelongsToOneRelation,
modelClass: this.relationBindKnex(ResourceField.default),
join: {
from: 'view_has_columns.fieldId',
to: 'resource_fields.id',
},
},
};
}
}

View File

@@ -27,7 +27,6 @@ export default class ViewRole extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
const ResourceField = require('models/ResourceField');
const View = require('models/View');
return {
@@ -42,18 +41,6 @@ export default class ViewRole extends TenantModel {
to: 'views.id',
},
},
/**
* View role model may belongs to resource field model.
*/
field: {
relation: Model.BelongsToOneRelation,
modelClass: this.relationBindKnex(ResourceField.default),
join: {
from: 'view_roles.fieldId',
to: 'resource_fields.id',
},
},
};
}
}

View File

@@ -15,7 +15,6 @@ import PaymentReceiveEntry from './PaymentReceiveEntry';
import Bill from './Bill';
import BillPayment from './BillPayment';
import BillPaymentEntry from './BillPaymentEntry';
import Resource from './Resource';
import View from './View';
import ItemEntry from './ItemEntry';
import InventoryTransaction from './InventoryTransaction';
@@ -39,7 +38,6 @@ export {
Bill,
BillPayment,
BillPaymentEntry,
Resource,
View,
ItemEntry,
InventoryTransaction,

View File

@@ -1,12 +0,0 @@
import { Resource } from 'models';
import BaseModelRepository from 'repositories/BaseModelRepository';
export default class ResourceRepository extends BaseModelRepository{
static async isExistsByName(name) {
const resourceNames = Array.isArray(name) ? name : [name];
const foundResources = await Resource.tenant().query().whereIn('name', resourceNames);
return foundResources.length > 0;
}
}

View File

@@ -0,0 +1,40 @@
import { View } from 'models';
import TenantRepository from 'repositories/TenantRepository';
export default class ViewRepository extends TenantRepository {
models: any;
cache: any;
repositories: any;
/**
* Constructor method.
* @param {number} tenantId - The given tenant id.
*/
constructor(
tenantId: number,
) {
super(tenantId);
this.models = this.tenancy.models(tenantId);
this.cache = this.tenancy.cache(tenantId);
this.repositories = this.tenancy.cache(tenantId);
}
/**
* Retrieve view model by the given id.
* @param {number} id -
*/
getById(id: number) {
const { View } = this.models;
return this.cache.get(`customView.id.${id}`, () => {
return View.query().findById(id);
});
}
allByResource() {
const resourceId = 1;
return this.cache.get(`customView.resource.id.${resourceId}`, () => {
return View.query().where('resource_id', resourceId);
});
}
}

View File

@@ -0,0 +1,29 @@
import { omit } from 'lodash';
import TenantRepository from 'repositories/TenantRepository';
export default class ViewRoleRepository extends TenantRepository {
models: any;
cache: any;
repositories: any;
/**
* Constructor method.
* @param {number} tenantId - The given tenant id.
*/
constructor(
tenantId: number,
) {
super(tenantId);
this.models = this.tenancy.models(tenantId);
this.cache = this.tenancy.cache(tenantId);
this.repositories = this.tenancy.cache(tenantId);
}
allByView(viewId: number) {
const { ViewRole } = this.models;
return this.cache.get(`viewRole.view.${viewId}`, async () => {
return ViewRole.query().where('view_id', viewId);
});
}
}

View File

@@ -2,15 +2,18 @@ import { Inject, Service } from 'typedi';
import { kebabCase } from 'lodash'
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
import { IAccountDTO, IAccount } from 'interfaces';
import { IAccountDTO, IAccount, IAccountsFilter } from 'interfaces';
import { difference } from 'lodash';
import { Account } from 'src/models';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service()
export default class AccountsService {
@Inject()
tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
@Inject('logger')
logger: any;
@@ -429,4 +432,22 @@ export default class AccountsService {
})
this.logger.info('[account] account have been activated successfully.', { tenantId, accountId });
}
/**
* Retrieve accounts datatable list.
* @param {number} tenantId
* @param {IAccountsFilter} accountsFilter
*/
async getAccountsList(tenantId: number, filter: IAccountsFilter) {
const { Account } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter);
this.logger.info('[accounts] trying to get accounts datatable list.', { tenantId, filter });
const accounts = await Account.query().onBuild((builder) => {
builder.withGraphFetched('type');
dynamicList.buildQuery()(builder);
});
return accounts;
}
}

View File

@@ -1,14 +1,16 @@
import { Inject, Service } from 'typedi';
import { difference } from 'lodash';
import { difference, rest } from 'lodash';
import JournalPoster from "services/Accounting/JournalPoster";
import JournalCommands from "services/Accounting/JournalCommands";
import ContactsService from 'services/Contacts/ContactsService';
import {
IVendorNewDTO,
IVendorEditDTO,
IVendor
IVendor,
IVendorsFilter
} from 'interfaces';
import { ServiceError } from 'exceptions';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
@Service()
@@ -19,6 +21,9 @@ export default class VendorsService {
@Inject()
tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Converts vendor to contact DTO.
* @param {IVendorNewDTO|IVendorEditDTO} vendorDTO
@@ -186,4 +191,20 @@ export default class VendorsService {
throw new ServiceError('some_vendors_have_bills');
}
}
/**
* Retrieve vendors datatable list.
* @param {number} tenantId - Tenant id.
* @param {IVendorsFilter} vendorsFilter - Vendors filter.
*/
async getVendorsList(tenantId: number, vendorsFilter: IVendorsFilter) {
const { Vendor } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Vendor, vendorsFilter);
const vendors = await Vendor.query().onBuild((builder) => {
dynamicFilter.buildQuery()(builder);
});
return vendors;
}
}

View File

@@ -0,0 +1,167 @@
import { Service, Inject } from "typedi";
import validator from 'is-my-json-valid';
import { Router, Request, Response, NextFunction } from 'express';
import { ServiceError } from 'exceptions';
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
} from 'lib/DynamicFilter';
import {
validateFieldKeyExistance,
validateFilterRolesFieldsExistance,
} from 'lib/ViewRolesBuilder';
import TenancyService from 'services/Tenancy/TenancyService';
import { IDynamicListFilterDTO, IFilterRole } from 'interfaces';
const ERRORS = {
VIEW_NOT_FOUND: 'view_not_found',
SORT_COLUMN_NOT_FOUND: 'sort_column_not_found',
FILTER_ROLES_FIELDS_NOT_FOUND: 'filter_roles_fields_not_found',
};
@Service()
export default class DynamicListService {
@Inject()
tenancy: TenancyService;
/**
* Middleware to catch services errors
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handlerErrorsToResponse(error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'sort_column_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'SORT.COLUMN.NOT.FOUND', code: 200 }],
});
}
if (error.errorType === 'view_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }]
})
}
if (error.errorType === 'filter_roles_fields_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'FILTER.ROLES.FIELDS.NOT.FOUND', code: 300 }],
});
}
if (error.errorType === 'stringified_filter_roles_invalid') {
return res.boom.badRequest(null, {
errors: [{ type: 'STRINGIFIED_FILTER_ROLES_INVALID', code: 400 }],
});
}
}
next(error);
}
/**
* Retreive custom view or throws error not found.
* @param {number} tenantId
* @param {number} viewId
* @return {Promise<IView>}
*/
async getCustomViewOrThrowError(tenantId: number, viewId: number) {
const { viewRepository } = this.tenancy.repositories(tenantId);
const view = await viewRepository.getById(viewId);
if (!view || view.resourceModel !== 'Account') {
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
}
return view;
}
/**
* Validates the sort column whether exists.
* @param {IModel} model
* @param {string} columnSortBy - Sort column
* @throws {ServiceError}
*/
validateSortColumnExistance(model: any, columnSortBy: string) {
const notExistsField = validateFieldKeyExistance(model.tableName, columnSortBy);
if (notExistsField) {
throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND);
}
}
/**
* Validates existance the fields of filter roles.
* @param {IModel} model
* @param {IFilterRole[]} filterRoles
* @throws {ServiceError}
*/
validateRolesFieldsExistance(model: any, filterRoles: IFilterRole[]) {
const invalidFieldsKeys = validateFilterRolesFieldsExistance(model.tableName, filterRoles);
if (invalidFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
}
}
/**
* Validates filter roles schema.
* @param {IFilterRole[]} filterRoles
*/
validateFilterRolesSchema(filterRoles: IFilterRole[]) {
const validate = validator({
required: true,
type: 'object',
properties: {
fieldKey: { required: true, type: 'string' },
value: { required: true, type: 'string' },
},
});
const invalidFields = filterRoles.filter((filterRole) => {
const isValid = validate(filterRole);
return isValid ? false : true;
});
if (invalidFields.length > 0) {
throw new ServiceError('stringified_filter_roles_invalid');
}
}
/**
* Dynamic listing.
* @param {number} tenantId
* @param {IModel} model
* @param {IAccountsFilter} filter
*/
async dynamicList(tenantId: number, model: any, filter: IDynamicListFilterDTO) {
const { viewRoleRepository } = this.tenancy.repositories(tenantId);
const dynamicFilter = new DynamicFilter(model.tableName);
// Custom view filter roles.
if (filter.customViewId) {
const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId);
const viewRoles = await viewRoleRepository.allByView(view.id);
const viewFilter = new DynamicFilterViews(viewRoles, view.rolesLogicExpression);
dynamicFilter.setFilter(viewFilter);
}
// Sort by the given column.
if (filter.columnSortBy) {
this.validateSortColumnExistance(model, filter.columnSortBy);;
const sortByFilter = new DynamicFilterSortBy(
filter.columnSortBy, filter.sortOrder
);
dynamicFilter.setFilter(sortByFilter);
}
// Filter roles.
if (filter.filterRoles.length > 0) {
this.validateFilterRolesSchema(filter.filterRoles);
this.validateRolesFieldsExistance(model, filter.filterRoles);
// Validate the accounts resource fields.
const filterRoles = new DynamicFilterFilterRoles(filter.filterRoles);
dynamicFilter.setFilter(filterRoles);
}
return dynamicFilter;
}
}

View File

@@ -1,75 +0,0 @@
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
} from 'lib/DynamicFilter';
import {
mapViewRolesToConditionals,
mapFilterRolesToDynamicFilter,
} from 'lib/ViewRolesBuilder';
export const DYNAMIC_LISTING_ERRORS = {
LOGIC_INVALID: 'VIEW.LOGIC.EXPRESSION.INVALID',
RESOURCE_HAS_NO_FIELDS: 'RESOURCE.HAS.NO.GIVEN.FIELDS',
};
export default class DynamicListing {
/**
* Constructor method.
* @param {DynamicListingBuilder} dynamicListingBuilder
* @return {DynamicListing|Error}
*/
constructor(dynamicListingBuilder) {
this.listingBuilder = dynamicListingBuilder;
this.dynamicFilter = new DynamicFilter(this.listingBuilder.modelClass.tableName);
return this.init();
}
/**
* Initialize the dynamic listing.
*/
init() {
// Initialize the column sort by.
if (this.listingBuilder.columnSortBy) {
const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_by,
filter.sort_order
);
this.dynamicFilter.setFilter(sortByFilter);
}
// Initialize the view filter roles.
if (this.listingBuilder.view && this.listingBuilder.view.roles.length > 0) {
const viewFilter = new DynamicFilterViews(
mapViewRolesToConditionals(this.listingBuilder.view.roles),
this.listingBuilder.view.rolesLogicExpression
);
if (!viewFilter.validateFilterRoles()) {
return new Error(DYNAMIC_LISTING_ERRORS.LOGIC_INVALID);
}
this.dynamicFilter.setFilter(viewFilter);
}
// Initialize the dynamic filter roles.
if (this.listingBuilder.filterRoles.length > 0) {
const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles),
accountsResource.fields
);
this.dynamicFilter.setFilter(filterRoles);
if (filterRoles.validateFilterRoles().length > 0) {
return new Error(DYNAMIC_LISTING_ERRORS.RESOURCE_HAS_NO_FIELDS);
}
}
return this;
}
/**
* Build query.
*/
buildQuery(){
return this.dynamicFilter.buildQuery();
}
}

View File

@@ -1,25 +0,0 @@
export default class DynamicListingBuilder {
addModelClass(modelClass) {
this.modelClass = modelClass;
}
addCustomViewId(customViewId) {
this.customViewId = customViewId;
}
addFilterRoles (filterRoles) {
this.filterRoles = filterRoles;
}
addSortBy(sortBy, sortOrder) {
this.sortBy = sortBy;
this.sortOrder = sortOrder;
}
addView(view) {
this.view = view;
}
}

View File

@@ -1,22 +0,0 @@
import { DYNAMIC_LISTING_ERRORS } from 'services/DynamicListing/DynamicListing';
export const dynamicListingErrorsToResponse = (error) => {
let _errors;
if (error.message === DYNAMIC_LISTING_ERRORS.LOGIC_INVALID) {
_errors.push({
type: DYNAMIC_LISTING_ERRORS.LOGIC_INVALID,
code: 200,
});
}
if (
error.message ===
DYNAMIC_LISTING_ERRORS.RESOURCE_HAS_NO_FIELDS
) {
_errors.push({
type: DYNAMIC_LISTING_ERRORS.RESOURCE_HAS_NO_FIELDS,
code: 300,
});
}
return _errors;
};

View File

@@ -7,6 +7,7 @@ import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import JournalCommands from 'services/Accounting/JournalCommands';
import { IExpense, IAccount, IExpenseDTO, IExpenseCategory, IExpensesService, ISystemUser } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
const ERRORS = {
EXPENSE_NOT_FOUND: 'expense_not_found',
@@ -23,6 +24,9 @@ export default class ExpensesService implements IExpensesService {
@Inject()
tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
@Inject('logger')
logger: any;
@@ -429,4 +433,24 @@ export default class ExpensesService implements IExpensesService {
this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds });
}
/**
* Retrieve expenses datatable lsit.
* @param {number} tenantId
* @param {IExpensesFilter} expensesFilter
* @return {IExpense[]}
*/
async getExpensesList(tenantId: number, expensesFilter: IExpensesFilter) {
const { Expense } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Expense, expensesFilter);
this.logger.info('[expense] trying to get expenses datatable list.', { tenantId, expensesFilter });
const expenses = await Expense.query().onBuild((builder) => {
builder.withGraphFetched('paymentAccount');
builder.withGraphFetched('user');
dynamicFilter.buildQuery()(builder);
});
return expenses;
}
}

View File

@@ -1,5 +1,7 @@
import { difference } from "lodash";
import { Service, Inject } from "typedi";
import { IItemsFilter } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
@Service()
@@ -7,6 +9,9 @@ export default class ItemsService {
@Inject()
tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
async newItem(tenantId: number, item: any) {
const { Item } = this.tenancy.models(tenantId);
const storedItem = await Item.query()
@@ -71,4 +76,24 @@ export default class ItemsService {
writeItemInventoryOpeningQuantity(tenantId: number, itemId: number, openingQuantity: number, averageCost: number) {
}
/**
* Retrieve items datatable list.
* @param {number} tenantId
* @param {IItemsFilter} itemsFilter
*/
async getItemsList(tenantId: number, itemsFilter: IItemsFilter) {
const { Item } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Item, itemsFilter);
const items = await Item.query().onBuild((builder) => {
builder.withGraphFetched('inventoryAccount');
builder.withGraphFetched('sellAccount');
builder.withGraphFetched('costAccount');
builder.withGraphFetched('category');
dynamicFilter.buildQuery()(builder);
});
return items;
}
}

View File

@@ -71,7 +71,7 @@ export default class HasTenancyService {
repositories(tenantId: number) {
return this.singletonService(tenantId, 'repositories', () => {
return tenantRepositoriesLoader(tenantId);
})
});
}
/**