mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
fix: resource advanced view filter.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
167
server/src/services/DynamicListing/DynamicListService.ts
Normal file
167
server/src/services/DynamicListing/DynamicListService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export default class HasTenancyService {
|
||||
repositories(tenantId: number) {
|
||||
return this.singletonService(tenantId, 'repositories', () => {
|
||||
return tenantRepositoriesLoader(tenantId);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user