refactoring: custom views service.

fix: constraints of delete item from storage.
fix: constraints of delete item category from storage.
fix: localize database seeds files.
fix: view meta data in accounts list response.
This commit is contained in:
Ahmed Bouhuolia
2020-10-05 19:09:56 +02:00
parent 0114ed9f8b
commit 99e6fe273f
64 changed files with 1593 additions and 1103 deletions

View File

@@ -3,7 +3,7 @@ import { difference } from 'lodash';
import { kebabCase } from 'lodash'
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
import { IAccountDTO, IAccount, IAccountsFilter } from 'interfaces';
import { IAccountDTO, IAccount, IAccountsFilter, IFilterMeta } from 'interfaces';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -260,17 +260,6 @@ export default class AccountsService {
return foundAccounts.length > 0;
}
public async getAccountByType(tenantId: number, accountTypeKey: string) {
const { AccountType, Account } = this.tenancy.models(tenantId);
const accountType = await AccountType.query()
.findOne('key', accountTypeKey);
const account = await Account.query()
.findOne('account_type_id', accountType.id);
return account;
}
/**
* Throws error if the account was prefined.
* @param {IAccount} account
@@ -468,9 +457,11 @@ export default class AccountsService {
* @param {number} tenantId
* @param {IAccountsFilter} accountsFilter
*/
public async getAccountsList(tenantId: number, filter: IAccountsFilter) {
public async getAccountsList(
tenantId: number,
filter: IAccountsFilter,
): Promise<{ accounts: IAccount[], filterMeta: IFilterMeta }> {
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 });
@@ -478,6 +469,10 @@ export default class AccountsService {
builder.withGraphFetched('type');
dynamicList.buildQuery()(builder);
});
return accounts;
return {
accounts,
filterMeta: dynamicList.getResponseMeta(),
};
}
}

View File

@@ -7,9 +7,12 @@ import {
ICustomerNewDTO,
ICustomerEditDTO,
ICustomer,
IPaginationMeta,
ICustomersFilter
} from 'interfaces';
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service()
export default class CustomersService {
@@ -19,12 +22,15 @@ export default class CustomersService {
@Inject()
tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Converts customer to contact DTO.
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
* @returns {IContactDTO}
*/
customerToContactDTO(customerDTO: ICustomerNewDTO|ICustomerEditDTO) {
private customerToContactDTO(customerDTO: ICustomerNewDTO | ICustomerEditDTO) {
return {
...omit(customerDTO, ['customerType']),
contactType: customerDTO.customerType,
@@ -39,7 +45,7 @@ export default class CustomersService {
* @param {ICustomerNewDTO} customerDTO
* @return {Promise<void>}
*/
async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) {
public async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) {
const contactDTO = this.customerToContactDTO(customerDTO)
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer');
@@ -59,7 +65,7 @@ export default class CustomersService {
* @param {number} tenantId
* @param {ICustomerEditDTO} customerDTO
*/
async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) {
public async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) {
const contactDTO = this.customerToContactDTO(customerDTO);
return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer');
}
@@ -70,7 +76,7 @@ export default class CustomersService {
* @param {number} customerId
* @return {Promise<void>}
*/
async deleteCustomer(tenantId: number, customerId: number) {
public async deleteCustomer(tenantId: number, customerId: number) {
const { Contact } = this.tenancy.models(tenantId);
await this.getCustomerByIdOrThrowError(tenantId, customerId);
@@ -88,10 +94,34 @@ export default class CustomersService {
* @param {number} tenantId
* @param {number} customerId
*/
async getCustomer(tenantId: number, customerId: number) {
public async getCustomer(tenantId: number, customerId: number) {
return this.contactService.getContact(tenantId, customerId, 'customer');
}
/**
* Retrieve customers paginated list.
* @param {number} tenantId - Tenant id.
* @param {ICustomersFilter} filter - Cusotmers filter.
*/
public async getCustomersList(
tenantId: number,
filter: ICustomersFilter
): Promise<{ customers: ICustomer[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { Contact } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Contact, filter);
const { results, pagination } = await Contact.query().onBuild((query) => {
query.modify('customer');
dynamicList.buildQuery()(query);
});
return {
customers: results,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
}
/**
* Writes customer opening balance journal entries.
* @param {number} tenantId

View File

@@ -1,6 +1,6 @@
import { Service, Inject } from "typedi";
import validator from 'is-my-json-valid';
import { Router, Request, Response, NextFunction } from 'express';
import { Request, Response, NextFunction } from 'express';
import { ServiceError } from 'exceptions';
import {
DynamicFilter,
@@ -12,8 +12,13 @@ import {
validateFieldKeyExistance,
validateFilterRolesFieldsExistance,
} from 'lib/ViewRolesBuilder';
import {
IDynamicListFilterDTO,
IFilterRole,
IDynamicListService,
IModel,
} from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import { IDynamicListFilterDTO, IFilterRole, IDynamicListService } from 'interfaces';
const ERRORS = {
VIEW_NOT_FOUND: 'view_not_found',
@@ -32,11 +37,11 @@ export default class DynamicListService implements IDynamicListService {
* @param {number} viewId
* @return {Promise<IView>}
*/
private async getCustomViewOrThrowError(tenantId: number, viewId: number) {
private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) {
const { viewRepository } = this.tenancy.repositories(tenantId);
const view = await viewRepository.getById(viewId);
if (!view || view.resourceModel !== 'Account') {
if (!view || view.resourceModel !== model.name) {
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
}
return view;
@@ -49,9 +54,9 @@ export default class DynamicListService implements IDynamicListService {
* @throws {ServiceError}
*/
private validateSortColumnExistance(model: any, columnSortBy: string) {
const notExistsField = validateFieldKeyExistance(model.tableName, columnSortBy);
const notExistsField = validateFieldKeyExistance(model, columnSortBy);
if (notExistsField) {
if (!notExistsField) {
throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND);
}
}
@@ -62,8 +67,10 @@ export default class DynamicListService implements IDynamicListService {
* @param {IFilterRole[]} filterRoles
* @throws {ServiceError}
*/
private validateRolesFieldsExistance(model: any, filterRoles: IFilterRole[]) {
const invalidFieldsKeys = validateFilterRolesFieldsExistance(model.tableName, filterRoles);
private validateRolesFieldsExistance(model: IModel, filterRoles: IFilterRole[]) {
const invalidFieldsKeys = validateFilterRolesFieldsExistance(model, filterRoles);
console.log(invalidFieldsKeys);
if (invalidFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
@@ -96,23 +103,21 @@ export default class DynamicListService implements IDynamicListService {
* Dynamic listing.
* @param {number} tenantId
* @param {IModel} model
* @param {IAccountsFilter} filter
* @param {IDynamicListFilterDTO} filter
*/
async dynamicList(tenantId: number, model: any, filter: IDynamicListFilterDTO) {
const { viewRoleRepository } = this.tenancy.repositories(tenantId);
const dynamicFilter = new DynamicFilter(model.tableName);
public async dynamicList(tenantId: number, model: IModel, filter: IDynamicListFilterDTO) {
const dynamicFilter = new DynamicFilter(model);
// Custom view filter roles.
if (filter.customViewId) {
const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId);
const viewRoles = await viewRoleRepository.allByView(view.id);
const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId, model);
const viewFilter = new DynamicFilterViews(viewRoles, view.rolesLogicExpression);
const viewFilter = new DynamicFilterViews(view);
dynamicFilter.setFilter(viewFilter);
}
// Sort by the given column.
if (filter.columnSortBy) {
this.validateSortColumnExistance(model, filter.columnSortBy);;
this.validateSortColumnExistance(model, filter.columnSortBy);
const sortByFilter = new DynamicFilterSortBy(
filter.columnSortBy, filter.sortOrder
@@ -138,7 +143,7 @@ export default class DynamicListService implements IDynamicListService {
* @param {Response} res
* @param {NextFunction} next
*/
handlerErrorsToResponse(error, req: Request, res: Response, next: NextFunction) {
public handlerErrorsToResponse(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'sort_column_not_found') {
return res.boom.badRequest(null, {
@@ -147,8 +152,8 @@ export default class DynamicListService implements IDynamicListService {
}
if (error.errorType === 'view_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }]
})
errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'filter_roles_fields_not_found') {
return res.boom.badRequest(null, {

View File

@@ -9,7 +9,7 @@ import { ServiceError } from "exceptions";
import TenancyService from 'services/Tenancy/TenancyService';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands';
import { IExpense, IAccount, IExpenseDTO, IExpensesService, ISystemUser } from 'interfaces';
import { IExpense, IExpensesFilter, IAccount, IExpenseDTO, IExpensesService, ISystemUser, IPaginationMeta } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events';
@@ -442,17 +442,23 @@ export default class ExpensesService implements IExpensesService {
* @param {IExpensesFilter} expensesFilter
* @return {IExpense[]}
*/
public async getExpensesList(tenantId: number, expensesFilter: IExpensesFilter) {
public async getExpensesList(
tenantId: number,
expensesFilter: IExpensesFilter
): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
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) => {
const { results, pagination } = await Expense.query().onBuild((builder) => {
builder.withGraphFetched('paymentAccount');
builder.withGraphFetched('user');
dynamicFilter.buildQuery()(builder);
});
return expenses;
}).pagination(expensesFilter.page - 1, expensesFilter.pageSize);
return {
expenses: results,
pagination, filterMeta:
dynamicFilter.getResponseMeta(),
};
}
}

View File

@@ -8,7 +8,6 @@ import {
IItemCategoriesFilter,
ISystemUser,
} from "interfaces";
import ItemCategory from "models/ItemCategory";
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
@@ -21,6 +20,7 @@ const ERRORS = {
SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND',
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS'
};
export default class ItemCategoriesService implements IItemCategoriesService {
@@ -202,6 +202,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
public async deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser) {
this.logger.info('[item_category] trying to delete item category.', { tenantId, itemCategoryId });
await this.getItemCategoryOrThrowError(tenantId, itemCategoryId);
await this.unassociateItemsWithCategories(tenantId, itemCategoryId);
const { ItemCategory } = this.tenancy.models(tenantId);
await ItemCategory.query().findById(itemCategoryId).delete();
@@ -233,10 +234,16 @@ export default class ItemCategoriesService implements IItemCategoriesService {
const dynamicList = await this.dynamicListService.dynamicList(tenantId, ItemCategory, filter);
const itemCategories = await ItemCategory.query().onBuild((query) => {
query.orderBy('createdAt', 'ASC');
dynamicList.buildQuery()(query);
});
return itemCategories;
return { itemCategories, filterMeta: dynamicList.getResponseMeta() };
}
private async unassociateItemsWithCategories(tenantId: number, itemCategoryId: number|number[]) {
const { Item } = this.tenancy.models(tenantId);
const ids = Array.isArray(itemCategoryId) ? itemCategoryId : [itemCategoryId];
await Item.query().whereIn('id', ids).patch({ category_id: null });
}
/**
@@ -247,7 +254,8 @@ export default class ItemCategoriesService implements IItemCategoriesService {
public async deleteItemCategories(tenantId: number, itemCategoriesIds: number[], authorizedUser: ISystemUser) {
this.logger.info('[item_category] trying to delete item categories.', { tenantId, itemCategoriesIds });
await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds);
await this.unassociateItemsWithCategories(tenantId, itemCategoriesIds);
await ItemCategory.query().whereIn('id', itemCategoriesIds).delete();
this.logger.info('[item_category] item categories deleted successfully.', { tenantId, itemCategoriesIds });
}

View File

@@ -4,7 +4,6 @@ import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from "exceptions";
import { Item } from "models";
const ERRORS = {
NOT_FOUND: 'NOT_FOUND',
@@ -17,6 +16,9 @@ const ERRORS = {
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS'
}
@Service()
@@ -222,6 +224,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] trying to delete item.', { tenantId, itemId });
await this.getItemOrThrowError(tenantId, itemId);
await this.validateHasNoInvoicesOrBills(tenantId, itemId);
await Item.query().findById(itemId).delete();
this.logger.info('[items] deleted successfully.', { tenantId, itemId });
@@ -269,6 +272,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] trying to delete items in bulk.', { tenantId, itemsIds });
await this.validateItemsIdsExists(tenantId, itemsIds);
await this.validateHasNoInvoicesOrBills(tenantId, itemsIds);
await Item.query().whereIn('id', itemsIds).delete();
this.logger.info('[items] deleted successfully in bulk.', { tenantId, itemsIds });
@@ -283,14 +287,39 @@ export default class ItemsService implements IItemsService {
const { Item } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Item, itemsFilter);
const items = await Item.query().onBuild((builder) => {
const { results, pagination } = await Item.query().onBuild((builder) => {
builder.withGraphFetched('inventoryAccount');
builder.withGraphFetched('sellAccount');
builder.withGraphFetched('costAccount');
builder.withGraphFetched('category');
dynamicFilter.buildQuery()(builder);
});
return items;
}).pagination(
itemsFilter.page - 1,
itemsFilter.pageSize,
);
return { items: results, pagination, filterMeta: dynamicFilter.getResponseMeta() };
}
/**
* Validates the given item or items have no associated invoices or bills.
* @param {number} tenantId - Tenant id.
* @param {number|number[]} itemId - Item id.
* @throws {ServiceError}
*/
private async validateHasNoInvoicesOrBills(tenantId: number, itemId: number[]|number) {
const { ItemEntry } = this.tenancy.models(tenantId);
const ids = Array.isArray(itemId) ? itemId : [itemId];
const foundItemEntries = await ItemEntry.query()
.whereIn('item_id', ids)
.whereIn('reference_type', ['SaleInvoice', 'Bill']);
if (foundItemEntries.length > 0) {
throw new ServiceError(ids.length > 1 ?
ERRORS.ITEMS_HAVE_ASSOCIATED_TRANSACTIONS :
ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTINS
);
}
}
}

View File

@@ -9,6 +9,7 @@ import {
ISystemUser,
IManualJournal,
IManualJournalEntryDTO,
IPaginationMeta,
} from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@@ -227,7 +228,7 @@ export default class ManualJournalsService implements IManuaLJournalsService {
}
/**
*
* Transform DTO to model.
* @param {IManualJournalEntryDTO[]} entries
*/
private transformDTOToEntriesModel(entries: IManualJournalEntryDTO[]) {
@@ -396,16 +397,23 @@ export default class ManualJournalsService implements IManuaLJournalsService {
* @param {number} tenantId
* @param {IManualJournalsFilter} filter
*/
public async getManualJournals(tenantId: number, filter: IManualJournalsFilter) {
public async getManualJournals(
tenantId: number,
filter: IManualJournalsFilter
): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { ManualJournal } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, ManualJournal, filter);
this.logger.info('[manual_journals] trying to get manual journals list.', { tenantId, filter });
const manualJournal = await ManualJournal.query().onBuild((builder) => {
const { results, pagination } = await ManualJournal.query().onBuild((builder) => {
dynamicList.buildQuery()(builder);
});
return manualJournal;
}).pagination(filter.page - 1, filter.pageSize);
return {
manualJournals: results,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
}
/**

View File

@@ -1,13 +1,14 @@
import { Inject, Service } from 'typedi';
import { omit, sumBy } from 'lodash';
import moment from 'moment';
import { IBillPaymentOTD, IBillPayment } from 'interfaces';
import { IBillPaymentOTD, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta } from 'interfaces';
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import JournalPosterService from 'services/Sales/JournalPosterService';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils';
/**
@@ -25,6 +26,9 @@ export default class BillPaymentsService {
@Inject()
journalService: JournalPosterService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Creates a new bill payment transcations and store it to the storage
* with associated bills entries and journal transactions.
@@ -39,7 +43,7 @@ export default class BillPaymentsService {
* @param {number} tenantId - Tenant id.
* @param {BillPaymentDTO} billPayment - Bill payment object.
*/
async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) {
public async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) {
const { Bill, BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
const billPayment = {
@@ -102,7 +106,7 @@ export default class BillPaymentsService {
* @param {BillPaymentDTO} billPayment
* @param {IBillPayment} oldBillPayment
*/
async editBillPayment(
public async editBillPayment(
tenantId: number,
billPaymentId: number,
billPaymentDTO,
@@ -171,7 +175,7 @@ export default class BillPaymentsService {
* @param {Integer} billPaymentId - The given bill payment id.
* @return {Promise}
*/
async deleteBillPayment(tenantId: number, billPaymentId: number) {
public async deleteBillPayment(tenantId: number, billPaymentId: number) {
const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query().where('id', billPaymentId).first();
@@ -203,7 +207,7 @@ export default class BillPaymentsService {
* @param {BillPayment} billPayment
* @param {Integer} billPaymentId
*/
async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) {
private async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) {
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
const paymentAmount = sumBy(billPayment.entries, 'payment_amount');
@@ -252,6 +256,35 @@ export default class BillPaymentsService {
]);
}
/**
* Retrieve bill payment paginted and filterable list.
* @param {number} tenantId
* @param {IBillPaymentsFilter} billPaymentsFilter
*/
public async listBillPayments(
tenantId: number,
billPaymentsFilter: IBillPaymentsFilter,
): Promise<{ billPayments: IBillPayment, pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { BillPayment } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, BillPayment, billPaymentsFilter);
this.logger.info('[bill_payment] try to get bill payments list.', { tenantId });
const { results, pagination } = await BillPayment.query().onBuild(builder => {
builder.withGraphFetched('vendor');
builder.withGraphFetched('paymentAccount');
dynamicFilter.buildQuery()(builder);
}).pagination(
billPaymentsFilter.page - 1,
billPaymentsFilter.pageSize,
);
return {
billPayments: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Retrieve bill payment with associated metadata.
* @param {number} billPaymentId - The bill payment id.

View File

@@ -1,5 +0,0 @@
export default class ResourceService {
}

View File

@@ -0,0 +1,78 @@
import { Service, Inject } from 'typedi';
import { camelCase, upperFirst } from 'lodash'
import { IModel } from 'interfaces';
import resourceFieldsKeys from 'data/ResourceFieldsKeys';
import TenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class ResourceService {
@Inject()
tenancy: TenancyService;
/**
*
* @param {string} resourceName
*/
getResourceFieldsRelations(modelName: string) {
const fieldsRelations = resourceFieldsKeys[modelName];
if (!fieldsRelations) {
throw new Error('Fields relation not found in thte given resource model.');
}
return fieldsRelations;
}
/**
* Transform resource to model name.
* @param {string} resourceName
*/
private resourceToModelName(resourceName: string): string {
return upperFirst(camelCase(resourceName));
}
/**
* Retrieve model from resource name in specific tenant.
* @param {number} tenantId
* @param {string} resourceName
*/
public getModel(tenantId: number, resourceName: string) {
const models = this.tenancy.models(tenantId);
const modelName = this.resourceToModelName(resourceName);
return models[modelName];
}
getModelFields(Model: IModel) {
const fields = Object.keys(Model.fields);
return fields.sort((a, b) => {
if (a < b) { return -1; }
if (a > b) { return 1; }
return 0;
});
}
/**
*
* @param {string} resourceName
*/
getResourceFields(Model: IModel) {
console.log(Model);
if (Model.resourceable) {
return this.getModelFields(Model);
}
return [];
}
/**
*
* @param {string} resourceName
*/
getResourceColumns(Model: IModel) {
if (Model.resourceable) {
return this.getModelFields(Model);
}
return [];
}
}

View File

@@ -10,6 +10,7 @@ import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository';
import CustomerRepository from 'repositories/CustomerRepository';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils';
/**
@@ -27,6 +28,9 @@ export default class PaymentReceiveService {
@Inject()
journalService: JournalPosterService;
@Inject()
dynamicListService: DynamicListingService;
@Inject('logger')
logger: any;
@@ -37,7 +41,7 @@ export default class PaymentReceiveService {
* @param {number} tenantId - Tenant id.
* @param {IPaymentReceive} paymentReceive
*/
async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) {
public async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) {
const {
PaymentReceive,
PaymentReceiveEntry,
@@ -107,7 +111,7 @@ export default class PaymentReceiveService {
* @param {IPaymentReceive} paymentReceive -
* @param {IPaymentReceive} oldPaymentReceive -
*/
async editPaymentReceive(
public async editPaymentReceive(
tenantId: number,
paymentReceiveId: number,
paymentReceive: any,
@@ -242,7 +246,7 @@ export default class PaymentReceiveService {
* @param {number} tenantId - Tenant id.
* @param {Integer} paymentReceiveId - Payment receive id.
*/
async getPaymentReceive(tenantId: number, paymentReceiveId: number) {
public async getPaymentReceive(tenantId: number, paymentReceiveId: number) {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.where('id', paymentReceiveId)
@@ -250,6 +254,30 @@ export default class PaymentReceiveService {
.first();
return paymentReceive;
}
/**
* Retrieve payment receives paginated and filterable list.
* @param {number} tenantId
* @param {IPaymentReceivesFilter} paymentReceivesFilter
*/
public async listPaymentReceives(tenantId: number, paymentReceivesFilter: IPaymentReceivesFilter) {
const { PaymentReceive } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, PaymentReceive, paymentReceivesFilter);
const { results, pagination } = await PaymentReceive.query().onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
dynamicFilter.buildQuery()(builder);
}).pagination(
paymentReceivesFilter.page - 1,
paymentReceivesFilter.pageSize,
);
return {
paymentReceives: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Retrieve the payment receive details with associated invoices.
@@ -310,7 +338,7 @@ export default class PaymentReceiveService {
* @param {IPaymentReceive} paymentReceive
* @param {Number} paymentReceiveId
*/
async recordPaymentReceiveJournalEntries(
private async recordPaymentReceiveJournalEntries(
tenantId: number,
paymentReceive: any,
paymentReceiveId?: number
@@ -370,7 +398,7 @@ export default class PaymentReceiveService {
* @param {Array} revertInvoices
* @return {Promise}
*/
async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) {
private async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const opers: Promise<T>[] = [];
@@ -392,7 +420,7 @@ export default class PaymentReceiveService {
* @param {Array} newPaymentReceiveEntries
* @return
*/
async saveChangeInvoicePaymentAmount(
private async saveChangeInvoicePaymentAmount(
tenantId: number,
paymentReceiveEntries: [],
newPaymentReceiveEntries: [],

View File

@@ -1,8 +1,10 @@
import { omit, difference, sumBy, mixin } from 'lodash';
import { Service, Inject } from 'typedi';
import { IEstimatesFilter, IFilterMeta, IPaginationMeta } from 'interfaces';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import { formatDateFields } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
/**
* Sale estimate service.
@@ -19,6 +21,9 @@ export default class SaleEstimateService {
@Inject('logger')
logger: any;
@Inject()
dynamicListService: DynamicListingService;
/**
* Creates a new estimate with associated entries.
* @async
@@ -208,4 +213,32 @@ export default class SaleEstimateService {
});
return foundEstimates.length > 0;
}
/**
* Retrieves estimates filterable and paginated list.
* @param {number} tenantId
* @param {IEstimatesFilter} estimatesFilter
*/
public async estimatesList(
tenantId: number,
estimatesFilter: IEstimatesFilter
): Promise<{ salesEstimates: ISaleEstimate[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { SaleEstimate } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleEstimate, estimatesFilter);
const { results, pagination } = await SaleEstimate.query().onBuild(builder => {
builder.withGraphFetched('customer');
builder.withGraphFetched('entries');
dynamicFilter.buildQuery()(builder);
}).pagination(
estimatesFilter.page - 1,
estimatesFilter.pageSize,
);
return {
salesEstimates: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
}

View File

@@ -1,11 +1,16 @@
import { Service, Inject } from 'typedi';
import { omit, sumBy, difference, pick, chain } from 'lodash';
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from 'interfaces';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta } from 'interfaces';
import JournalPoster from 'services/Accounting/JournalPoster';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import InventoryService from 'services/Inventory/Inventory';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils';
/**
@@ -26,6 +31,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@Inject('logger')
logger: any;
@Inject()
dynamicListService: DynamicListingService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
@@ -34,7 +45,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {ISaleInvoice} saleInvoiceDTO -
* @return {ISaleInvoice}
*/
async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) {
public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) {
const { SaleInvoice, Customer, ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
@@ -94,7 +105,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {Number} saleInvoiceId -
* @param {ISaleInvoice} saleInvoice -
*/
async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) {
public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) {
const { SaleInvoice, ItemEntry, Customer } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
@@ -152,7 +163,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @async
* @param {Number} saleInvoiceId - The given sale invoice id.
*/
async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) {
public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) {
const {
SaleInvoice,
ItemEntry,
@@ -215,7 +226,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {number} saleInvoiceId -
* @param {boolean} override -
*/
recordInventoryTranscactions(
private recordInventoryTranscactions(
tenantId: number,
saleInvoice,
saleInvoiceId: number,
@@ -243,7 +254,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {string} transactionType
* @param {number} transactionId
*/
async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
private async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
const { InventoryTransaction } = this.tenancy.models(tenantId);
const opers: Promise<[]>[] = [];
@@ -280,7 +291,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @async
* @param {Number} saleInvoiceId
*/
async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) {
public async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) {
const { SaleInvoice } = this.tenancy.models(tenantId);
return SaleInvoice.query()
.where('id', saleInvoiceId)
@@ -405,4 +416,27 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
journal.saveBalance(),
]);
}
/**
* Retrieve sales invoices filterable and paginated list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async salesInvoicesList(tenantId: number, salesInvoicesFilter: ISalesInvoicesFilter):
Promise<{ salesInvoices: ISaleInvoice[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleInvoice, salesInvoicesFilter);
this.logger.info('[sale_invoice] try to get sales invoices list.', { tenantId, salesInvoicesFilter });
const { results, pagination } = await SaleInvoice.query().onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
}).pagination(
salesInvoicesFilter.page - 1,
salesInvoicesFilter.pageSize,
);
return { salesInvoices: results, pagination, filterMeta: dynamicFilter.getResponseMeta() };
}
}

View File

@@ -4,12 +4,17 @@ import JournalPosterService from 'services/Sales/JournalPosterService';
import HasItemEntries from 'services/Sales/HasItemsEntries';
import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
import { IFilterMeta, IPaginationMeta } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service()
export default class SalesReceiptService {
@Inject()
tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
journalService: JournalPosterService;
@@ -22,7 +27,7 @@ export default class SalesReceiptService {
* @param {ISaleReceipt} saleReceipt
* @return {Object}
*/
async createSaleReceipt(tenantId: number, saleReceiptDTO: any) {
public async createSaleReceipt(tenantId: number, saleReceiptDTO: any) {
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
@@ -55,7 +60,7 @@ export default class SalesReceiptService {
* @param {ISaleReceipt} saleReceipt
* @return {void}
*/
async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) {
public async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) {
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
@@ -88,7 +93,7 @@ export default class SalesReceiptService {
* @param {Integer} saleReceiptId
* @return {void}
*/
async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
const deleteSaleReceiptOper = SaleReceipt.query()
.where('id', saleReceiptId)
@@ -160,4 +165,35 @@ export default class SalesReceiptService {
return saleReceipt;
}
/**
* Retrieve sales receipts paginated and filterable list.
* @param {number} tenantId
* @param {ISaleReceiptFilter} salesReceiptsFilter
*/
public async salesReceiptsList(
tenantId: number,
salesReceiptsFilter: ISaleReceiptFilter,
): Promise<{ salesReceipts: ISaleReceipt[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { SaleReceipt } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleReceipt, salesReceiptsFilter);
this.logger.info('[sale_receipt] try to get sales receipts list.', { tenantId });
const { results, pagination } = await SaleReceipt.query().onBuild((builder) => {
builder.withGraphFetched('depositAccount');
builder.withGraphFetched('customer');
builder.withGraphFetched('entries');
dynamicFilter.buildQuery()(builder);
}).pagination(
salesReceiptsFilter.page - 1,
salesReceiptsFilter.pageSize,
);
return {
salesReceipts: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
}

View File

@@ -27,7 +27,6 @@ export default class HasTenancyService {
singletonService(tenantId: number, key: string, callback: Function) {
const container = this.tenantContainer(tenantId);
const Logger = Container.get('logger');
const hasServiceInstnace = container.has(key);
if (!hasServiceInstnace) {
@@ -74,12 +73,24 @@ export default class HasTenancyService {
});
}
/**
* Sets i18n locals function.
* @param {number} tenantId
* @param locals
*/
setI18nLocals(tenantId: number, locals: any) {
return this.singletonService(tenantId, 'i18n', () => {
return locals;
})
}
/**
* Retrieve i18n locales methods.
* @param {number} tenantId - Tenant id.
*/
i18n(tenantId: number) {
return this.singletonService(tenantId, 'i18n', () => {
throw new Error('I18n locals is not set yet.');
});
}

View File

@@ -1,20 +1,24 @@
import { Service, Inject } from "typedi";
import { pick, difference } from 'lodash';
import { difference } from 'lodash';
import { ServiceError } from 'exceptions';
import {
IViewsService,
IViewDTO,
IView,
IViewRole,
IViewHasColumn,
IViewEditDTO,
} from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import ResourceService from "services/Resource/ResourceService";
import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder';
const ERRORS = {
VIEW_NOT_FOUND: 'VIEW_NOT_FOUND',
VIEW_PREDEFINED: 'VIEW_PREDEFINED',
INVALID_LOGIC_EXPRESSION: 'INVALID_LOGIC_EXPRESSION',
VIEW_NAME_NOT_UNIQUE: 'VIEW_NAME_NOT_UNIQUE',
LOGIC_EXPRESSION_INVALID: 'INVALID_LOGIC_EXPRESSION',
RESOURCE_FIELDS_KEYS_NOT_FOUND: 'RESOURCE_FIELDS_KEYS_NOT_FOUND',
RESOURCE_COLUMNS_KEYS_NOT_FOUND: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND',
RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND'
};
@Service()
@@ -25,29 +29,131 @@ export default class ViewsService implements IViewsService {
@Inject('logger')
logger: any;
@Inject()
resourceService: ResourceService;
/**
* Listing resource views.
* @param {number} tenantId
* @param {string} resourceModel
* @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);
}
public async listResourceViews(tenantId: number, resourceModel: string): Promise<IView[]> {
this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModel });
validateResourceFieldsExistance() {
}
validateResourceColumnsExistance() {
}
getView(tenantId: number, viewId: number) {
// Validate the resource model name is valid.
this.getResourceModelOrThrowError(tenantId, resourceModel);
const { viewRepository } = this.tenancy.repositories(tenantId);
return viewRepository.allByResource(resourceModel);
}
/**
* Validate model resource conditions fields existance.
* @param {string} resourceName
* @param {IViewRoleDTO[]} viewRoles
*/
private validateResourceRolesFieldsExistance(ResourceModel: IModel, viewRoles: IViewRoleDTO[]) {
const resourceFieldsKeys = this.resourceService.getResourceFields(ResourceModel);
const fieldsKeys = viewRoles.map(viewRole => viewRole.fieldKey);
const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys);
if (notFoundFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.RESOURCE_FIELDS_KEYS_NOT_FOUND);
}
return notFoundFieldsKeys;
}
/**
* Validates model resource columns existance.
* @param {string} resourceName
* @param {IViewColumnDTO[]} viewColumns
*/
private validateResourceColumnsExistance(ResourceModel: IModel, viewColumns: IViewColumnDTO[]) {
const resourceFieldsKeys = this.resourceService.getResourceColumns(ResourceModel);
const fieldsKeys = viewColumns.map((viewColumn: IViewColumnDTO) => viewColumn.fieldKey);
const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys);
if (notFoundFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.RESOURCE_COLUMNS_KEYS_NOT_FOUND);
}
return notFoundFieldsKeys;
}
/**
* Retrieve the given view details with associated conditions and columns.
* @param {number} tenantId - Tenant id.
* @param {number} viewId - View id.
*/
public getView(tenantId: number, viewId: number): Promise<IView> {
this.logger.info('[view] trying to get view from storage.', { tenantId, viewId });
return this.getViewOrThrowError(tenantId, viewId);
}
/**
* Retrieve view or throw not found error.
* @param {number} tenantId - Tenant id.
* @param {number} viewId - View id.
*/
private async getViewOrThrowError(tenantId: number, viewId: number): Promise<IView> {
const { viewRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[view] trying to get view from storage.', { tenantId, viewId });
const view = await viewRepository.getById(viewId);
if (!view) {
this.logger.info('[view] view not found.', { tenantId, viewId });
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
}
return view;
}
/**
* Retrieve resource model from resource name or throw not found error.
* @param {number} tenantId
* @param {number} resourceModel
*/
private getResourceModelOrThrowError(tenantId: number, resourceModel: string): IModel {
const ResourceModel = this.resourceService.getModel(tenantId, resourceModel);
if (!ResourceModel || !ResourceModel.resourceable) {
throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND);
}
return ResourceModel;
}
/**
* Validates view name uniqiness in the given resource.
* @param {number} tenantId
* @param {stirng} resourceModel
* @param {string} viewName
* @param {number} notViewId
*/
private async validateViewNameUniquiness(
tenantId: number,
resourceModel: string,
viewName: string,
notViewId?: number
): void {
const { View } = this.tenancy.models(tenantId);
const foundViews = await View.query()
.where('resource_model', resourceModel)
.where('name', viewName)
.onBuild((builder) => {
if (notViewId) {
builder.whereNot('id', notViewId);
}
});
if (foundViews.length > 0) {
throw new ServiceError(ERRORS.VIEW_NAME_NOT_UNIQUE);
}
}
/**
* Creates a new custom view to specific resource.
* ----
* Precedures.
* ----
* - Validate resource fields existance.
@@ -60,116 +166,78 @@ export default class ViewsService implements IViewsService {
* @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);
public async newView(tenantId: number, viewDTO: IViewDTO): Promise<IView> {
const { viewRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[views] trying to create a new view.', { tenantId, viewDTO });
// Validate the resource name is exists and resourcable.
const ResourceModel = this.getResourceModelOrThrowError(tenantId, viewDTO.resourceModel);
// Validate view name uniquiness.
await this.validateViewNameUniquiness(tenantId, viewDTO.resourceModel, viewDTO.name);
// Validate the given fields keys exist on the storage.
this.validateResourceRolesFieldsExistance(ResourceModel, viewDTO.roles);
// Validate the given columnable fields keys exists on the storage.
this.validateResourceColumnsExistance(ResourceModel, viewDTO.columns);
// Validates the view conditional logic expression.
if (!validateRolesLogicExpression(viewDTO.logicExpression, viewDTO.roles)) {
throw new ServiceError(ERRORS.INVALID_LOGIC_EXPRESSION);
throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID);
}
// Save view details.
const view = await View.query().insert({
name: viewDTO.name,
const view = await viewRepository.insert({
predefined: false,
name: viewDTO.name,
rolesLogicExpression: viewDTO.logicExpression,
resourceModel: viewDTO.resourceModel,
roles: viewDTO.roles,
columns: viewDTO.columns,
});
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);
this.logger.info('[views] inserted to the storage successfully.', { tenantId, viewDTO });
return view;
}
/**
* Edits view details, roles and columns on the storage.
* --------
* Precedures.
* --------
* - Validate view existance.
* - Validate view resource fields existance.
* - Validate view resource columns existance.
* - Validate view logic expression.
* - Delete old view columns and roles.
* - Re-save view columns and roles.
*
* @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();
public async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise<void> {
const { View } = this.tenancy.models(tenantId);
this.logger.info('[view] trying to edit custom view.', { tenantId, viewId });
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);
// Retrieve view details or throw not found error.
const view = await this.getViewOrThrowError(tenantId, viewId);
// The difference between the stored resource fields and submit fields keys.
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
// Validate the resource name is exists and resourcable.
const ResourceModel = this.getResourceModelOrThrowError(tenantId, view.resourceModel);
// 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 view name uniquiness.
await this.validateViewNameUniquiness(tenantId, view.resourceModel, viewEditDTO.name, viewId);
// Validate the given fields keys exist on the storage.
this.validateResourceRolesFieldsExistance(ResourceModel, view.roles);
// Validate the given columnable fields keys exists on the storage.
this.validateResourceColumnsExistance(ResourceModel, view.columns);
// 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 });
if (!validateRolesLogicExpression(viewEditDTO.logicExpression, viewEditDTO.roles)) {
throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID);
}
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)
@@ -177,78 +245,15 @@ export default class ViewsService implements IViewsService {
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);
this.logger.info('[view] edited successfully.', { tenantId, viewId });
}
/**
* Retrieve views details of the given id or throw not found error.
* @private
* @param {number} tenantId
* @param {number} viewId
* @return {Promise<IView>}
*/
private async getViewByIdOrThrowError(tenantId: number, viewId: number): Promise<IView> {
const { View } = this.tenancy.models(tenantId);
@@ -267,6 +272,7 @@ export default class ViewsService implements IViewsService {
* Deletes the given view with associated roles and columns.
* @param {number} tenantId - Tenant id.
* @param {number} viewId - View id.
* @return {Promise<void>}
*/
public async deleteView(tenantId: number, viewId: number): Promise<void> {
const { View } = this.tenancy.models(tenantId);