fix: accounts TS interfaces.

This commit is contained in:
Ahmed Bouhuolia
2020-09-28 17:45:26 +02:00
parent f035c7f10a
commit 108b52962a
5 changed files with 529 additions and 9 deletions

View File

@@ -83,8 +83,9 @@ export default class AccountsController extends BaseController{
this.catchServiceErrors,
);
router.delete(
'/',
this.bulkDeleteSchema,
'/', [
...this.bulkDeleteSchema,
],
this.validationResult,
asyncMiddleware(this.deleteBulkAccounts.bind(this)),
this.catchServiceErrors,
@@ -279,8 +280,9 @@ export default class AccountsController extends BaseController{
const { ids: accountsIds } = req.query;
try {
const isActive = (type === 'activate' ? 1 : 0);
await this.accountsService.activateAccounts(tenantId, accountsIds, isActive)
const isActive = (type === 'activate' ? true : false);
await this.accountsService.activateAccounts(tenantId, accountsIds, isActive);
return res.status(200).send({ ids: accountsIds });
} catch (error) {
next(error);

View File

@@ -0,0 +1,217 @@
import { Inject, Service } from 'typedi';
import { Router, Request, NextFunction, Response } from 'express';
import {
check,
query,
param,
oneOf,
validationResult,
} from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import {
validateViewRoles,
} from 'lib/ViewRolesBuilder';
import ViewsService from 'services/Views/ViewsService';
import BaseController from './BaseController';
import { IViewDTO } from 'interfaces';
import { ServiceError } from 'exceptions';
@Service()
export default class ViewsController extends BaseController{
@Inject()
viewsService: ViewsService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get('/', [
...this.viewDTOSchemaValidation,
],
asyncMiddleware(this.listViews)
);
router.post('/', [
...this.viewDTOSchemaValidation,
],
asyncMiddleware(this.createView)
);
router.post('/:view_id', [
...this.viewDTOSchemaValidation,
],
asyncMiddleware(this.editView)
);
router.delete('/:view_id', [
...this.viewParamSchemaValidation
],
asyncMiddleware(this.deleteView));
router.get('/:view_id', [
...this.viewParamSchemaValidation
]
asyncMiddleware(this.getView)
);
router.get('/:view_id/resource', [
...this.viewParamSchemaValidation
],
asyncMiddleware(this.getViewResource)
);
return router;
}
get viewDTOSchemaValidation() {
return [
check('resource_name').exists().escape().trim(),
check('name').exists().escape().trim(),
check('logic_expression').exists().trim().escape(),
check('roles').isArray({ min: 1 }),
check('roles.*.field_key').exists().escape().trim(),
check('roles.*.comparator').exists(),
check('roles.*.value').exists(),
check('roles.*.index').exists().isNumeric().toInt(),
check('columns').exists().isArray({ min: 1 }),
check('columns.*.key').exists().escape().trim(),
check('columns.*.index').exists().isNumeric().toInt(),
];
}
get viewParamSchemaValidation() {
return [
]
}
/**
* List all views that associated with the given resource.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
listViews(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = req.query;
try {
const views = this.viewsService.listViews(tenantId, filter);
return res.status(200).send({ views });
} catch (error) {
next(error);
}
}
/**
*
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
getView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
try {
const view = this.viewsService.getView(tenantId, viewId);
return res.status(200).send({ view });
} catch (error) {
next(error);
}
}
/**
* Creates a new view.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
createView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const viewDTO: IViewDTO = this.matchedBodyData(req);
try {
await this.viewsService.newView(tenantId, viewDTO);
return res.status(200).send({ id: 1 });
} catch (error) {
next(error);
}
}
/**
* Edits views metadata.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
editView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: viewId } = req.params;
const { body: viewEditDTO } = req;
try {
await this.viewsService.editView(tenantId, viewId, viewEditDTO);
return res.status(200).send({ id: viewId });
} catch (error) {
next(error);
}
}
/**
* Deletes the given view.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
deleteView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: viewId } = req.params;
try {
await this.viewsService.deleteView(tenantId, viewId);
return res.status(200).send();
} catch (error) {
next(error);
}
}
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'INVALID_LOGIC_EXPRESSION') {
return res.boom.badRequest(null, {
errors: [{ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }],
});
}
if (error.errorType === '') {
return res.boom.badRequest(null, {
errors: [{ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100 }],
});
}
if (error.errorType === '') {
return res.boom.badRequest(null, {
errors: [{ type: 'COLUMNS_NOT_EXIST', code: 200 }],
});
}
if (error.errorType === 'VIEW_NOT_FOUND') {
return res.boom.notFound(null, {
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
});
}
if (error.errorType === 'VIEW_PREDEFINED') {
return res.boom.badRequest(null, {
errors: [{ type: 'PREDEFINED_VIEW', code: 200 }],
});
}
}
}
};

View File

@@ -4,14 +4,22 @@ export interface IAccountDTO {
name: string,
code: string,
description: string,
accountTypeNumber: number,
accountTypeId: number,
parentAccountId: number,
active: boolean,
};
export interface IAccount {
name: string,
slug: string,
code: string,
description: string,
accountTypeNumber: number,
accountTypeId: number,
parentAccountId: number,
active: boolean,
predefined: boolean,
amount: number,
currencyCode: string,
};
export interface IAccountsFilter extends IDynamicListFilterDTO {

View File

@@ -263,7 +263,7 @@ export default class AccountsService {
* @param {IAccount} account
*/
private throwErrorIfAccountPredefined(account: IAccount) {
if (account.prefined) {
if (account.predefined) {
throw new ServiceError('account_predefined');
}
}
@@ -344,6 +344,11 @@ export default class AccountsService {
return storedAccounts;
}
/**
* Validate whether one of the given accounts is predefined.
* @param {IAccount[]} accounts -
* @return {IAccount[]} - Predefined accounts
*/
private validatePrefinedAccounts(accounts: IAccount[]) {
const predefined = accounts.filter((account: IAccount) => account.predefined);
@@ -405,7 +410,7 @@ export default class AccountsService {
*/
public async activateAccounts(tenantId: number, accountsIds: number[], activate: boolean = true) {
const { Account } = this.tenancy.models(tenantId);
const accounts = await this.getAccountsOrThrowError(tenantId, accountsIds);
await this.getAccountsOrThrowError(tenantId, accountsIds);
this.logger.info('[account] trying activate/inactive the given accounts ids.', { accountsIds });
await Account.query().whereIn('id', accountsIds)
@@ -438,7 +443,7 @@ export default class AccountsService {
* @param {number} tenantId
* @param {IAccountsFilter} accountsFilter
*/
async getAccountsList(tenantId: number, filter: IAccountsFilter) {
public async getAccountsList(tenantId: number, filter: IAccountsFilter) {
const { Account } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter);

View File

@@ -0,0 +1,288 @@
import { Service, Inject } from "typedi";
import { pick, difference } from 'lodash';
import { ServiceError } from 'exceptions';
import {
IViewsService,
IViewDTO,
IView,
IViewRole,
IViewHasColumn,
} from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder';
const ERRORS = {
VIEW_NOT_FOUND: 'VIEW_NOT_FOUND',
VIEW_PREDEFINED: 'VIEW_PREDEFINED',
INVALID_LOGIC_EXPRESSION: 'INVALID_LOGIC_EXPRESSION',
};
@Service()
export default class ViewsService implements IViewsService {
@Inject()
tenancy: TenancyService;
@Inject('logger')
logger: any;
/**
* Listing resource views.
* @param {number} tenantId
* @param {string} resourceModel
*/
public async listViews(tenantId: number, resourceModel: string) {
const { View } = this.tenancy.models(tenantId);
return View.query().where('resource_model', resourceModel);
}
validateResourceFieldsExistance() {
}
validateResourceColumnsExistance() {
}
getView(tenantId: number, viewId: number) {
}
/**
* Precedures.
* ----
* - Validate resource fields existance.
* - Validate resource columns existance.
* - Validate view logic expression.
* - Store view to the storage.
* - Store view columns to the storage.
* - Store view roles/conditions to the storage.
* ---------
* @param {number} tenantId - Tenant id.
* @param {IViewDTO} viewDTO - View DTO.
*/
async newView(tenantId: number, viewDTO: IViewDTO): Promise<void> {
const { View, ViewColumn, ViewRole } = this.tenancy.models(tenantId);
this.logger.info('[views] trying to create a new view.', { tenantId, viewDTO });
// Validates the view conditional logic expression.
if (!validateRolesLogicExpression(viewDTO.logicExpression, viewDTO.roles)) {
throw new ServiceError(ERRORS.INVALID_LOGIC_EXPRESSION);
}
// Save view details.
const view = await View.query().insert({
name: viewDTO.name,
predefined: false,
rolesLogicExpression: viewDTO.logicExpression,
});
this.logger.info('[views] inserted to the storage.', { tenantId, viewDTO });
// Save view roles async operations.
const saveViewRolesOpers = [];
viewDTO.roles.forEach((role) => {
const saveViewRoleOper = ViewRole.query().insert({
...pick(role, ['fieldKey', 'comparator', 'value', 'index']),
viewId: view.id,
});
saveViewRolesOpers.push(saveViewRoleOper);
});
viewDTO.columns.forEach((column) => {
const saveViewColumnOper = ViewColumn.query().insert({
viewId: view.id,
index: column.index,
});
saveViewRolesOpers.push(saveViewColumnOper);
});
this.logger.info('[views] roles and columns inserted to the storage.', { tenantId, viewDTO });
await Promise.all(saveViewRolesOpers);
}
/**
*
* @param {number} tenantId
* @param {number} viewId
* @param {IViewEditDTO}
*/
async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO) {
const { View, ViewRole, ViewColumn } = req.models;
const view = await View.query().where('id', viewId)
.withGraphFetched('roles.field')
.withGraphFetched('columns')
.first();
const errorReasons = [];
const fieldsSlugs = viewEditDTO.roles.map((role) => role.field_key);
const resourceFieldsKeys = resource.fields.map((f) => f.key);
const resourceFieldsKeysMap = new Map(resource.fields.map((field) => [field.key, field]));
const columnsKeys = viewEditDTO.columns.map((c) => c.key);
// The difference between the stored resource fields and submit fields keys.
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
// Validate not found resource fields keys.
if (notFoundFields.length > 0) {
errorReasons.push({
type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields,
});
}
// The difference between the stored resource fields and the submit columns keys.
const notFoundColumns = difference(columnsKeys, resourceFieldsKeys);
// Validate not found view columns.
if (notFoundColumns.length > 0) {
errorReasons.push({ type: 'RESOURCE_COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns });
}
// Validates the view conditional logic expression.
if (!validateViewRoles(viewEditDTO.roles, viewEditDTO.logicExpression)) {
errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 });
}
const viewRolesIds = view.roles.map((r) => r.id);
const viewColumnsIds = view.columns.map((c) => c.id);
const formUpdatedRoles = viewEditDTO.roles.filter((r) => r.id);
const formInsertRoles = viewEditDTO.roles.filter((r) => !r.id);
const formRolesIds = formUpdatedRoles.map((r) => r.id);
const formUpdatedColumns = viewEditDTO.columns.filter((r) => r.id);
const formInsertedColumns = viewEditDTO.columns.filter((r) => !r.id);
const formColumnsIds = formUpdatedColumns.map((r) => r.id);
const rolesIdsShouldDeleted = difference(viewRolesIds, formRolesIds);
const columnsIdsShouldDelete = difference(viewColumnsIds, formColumnsIds);
const notFoundViewRolesIds = difference(formRolesIds, viewRolesIds);
const notFoundViewColumnsIds = difference(viewColumnsIds, viewColumnsIds);
// Validate the not found view roles ids.
if (notFoundViewRolesIds.length) {
errorReasons.push({ type: 'VIEW.ROLES.IDS.NOT.FOUND', code: 500, ids: notFoundViewRolesIds });
}
// Validate the not found view columns ids.
if (notFoundViewColumnsIds.length) {
errorReasons.push({ type: 'VIEW.COLUMNS.IDS.NOT.FOUND', code: 600, ids: notFoundViewColumnsIds });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const asyncOpers = [];
// Save view details.
await View.query()
.where('id', view.id)
.patch({
name: viewEditDTO.name,
roles_logic_expression: viewEditDTO.logicExpression,
});
// Update view roles.
if (formUpdatedRoles.length > 0) {
formUpdatedRoles.forEach((role) => {
const fieldModel = resourceFieldsKeysMap.get(role.field_key);
const updateOper = ViewRole.query()
.where('id', role.id)
.update({
...pick(role, ['comparator', 'value', 'index']),
field_id: fieldModel.id,
});
asyncOpers.push(updateOper);
});
}
// Insert a new view roles.
if (formInsertRoles.length > 0) {
formInsertRoles.forEach((role) => {
const fieldModel = resourceFieldsKeysMap.get(role.field_key);
const insertOper = ViewRole.query()
.insert({
...pick(role, ['comparator', 'value', 'index']),
field_id: fieldModel.id,
view_id: view.id,
});
asyncOpers.push(insertOper);
});
}
// Delete view roles.
if (rolesIdsShouldDeleted.length > 0) {
const deleteOper = ViewRole.query()
.whereIn('id', rolesIdsShouldDeleted)
.delete();
asyncOpers.push(deleteOper);
}
// Insert a new view columns to the storage.
if (formInsertedColumns.length > 0) {
formInsertedColumns.forEach((column) => {
const fieldModel = resourceFieldsKeysMap.get(column.key);
const insertOper = ViewColumn.query()
.insert({
field_id: fieldModel.id,
index: column.index,
view_id: view.id,
});
asyncOpers.push(insertOper);
});
}
// Update the view columns on the storage.
if (formUpdatedColumns.length > 0) {
formUpdatedColumns.forEach((column) => {
const updateOper = ViewColumn.query()
.where('id', column.id)
.update({
index: column.index,
});
asyncOpers.push(updateOper);
});
}
// Delete the view columns from the storage.
if (columnsIdsShouldDelete.length > 0) {
const deleteOper = ViewColumn.query()
.whereIn('id', columnsIdsShouldDelete)
.delete();
asyncOpers.push(deleteOper);
}
await Promise.all(asyncOpers);
}
/**
* Retrieve views details of the given id or throw not found error.
* @param {number} tenantId
* @param {number} viewId
*/
private async getViewByIdOrThrowError(tenantId: number, viewId: number): Promise<IView> {
const { View } = this.tenancy.models(tenantId);
this.logger.info('[views] get stored view.', { tenantId, viewId });
const view = await View.query().findById(viewId);
if (!view) {
this.logger.info('[views] the given id not found.', { tenantId, viewId });
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
}
return view;
}
/**
* Deletes the given view with associated roles and columns.
* @param {number} tenantId - Tenant id.
* @param {number} viewId - View id.
*/
public async deleteView(tenantId: number, viewId: number): Promise<void> {
const { View } = this.tenancy.models(tenantId);
this.logger.info('[views] trying to delete the given view.', { tenantId, viewId });
const view = await this.getViewByIdOrThrowError(tenantId, viewId);
if (view.predefined) {
this.logger.info('[views] cannot delete predefined.', { tenantId, viewId });
throw new ServiceError(ERRORS.VIEW_PREDEFINED);
}
await Promise.all([
view.$relatedQuery('roles').delete(),
view.$relatedQuery('columns').delete(),
]);
await View.query().where('id', view.id).delete();
this.logger.info('[views] deleted successfully.', { tenantId, viewId });
}
}