refactor: wip migrate ot nestjs

This commit is contained in:
Ahmed Bouhuolia
2024-12-19 12:48:24 +02:00
parent bfff56c470
commit 93bf6d9d3d
73 changed files with 4683 additions and 96 deletions

View File

@@ -0,0 +1,89 @@
import { forEach } from 'lodash';
import { DynamicFilterAbstractor } from './DynamicFilterAbstractor';
import { IDynamicFilter, IFilterRole, IModel } from '@/interfaces';
import { BaseModel } from '@/models/Model';
export class DynamicFilter extends DynamicFilterAbstractor {
private model: BaseModel;
private dynamicFilters: IDynamicFilter[];
/**
* Constructor.
* @param {String} tableName -
*/
constructor(model: BaseModel) {
super();
this.model = model;
this.dynamicFilters = [];
}
/**
* Registers the given dynamic filter.
* @param {IDynamicFilter} filterRole - Filter role.
*/
public setFilter = (dynamicFilter: IDynamicFilter) => {
dynamicFilter.setModel(this.model);
dynamicFilter.onInitialize();
this.dynamicFilters.push(dynamicFilter);
};
/**
* Retrieve dynamic filter build queries.
* @returns
*/
private dynamicFiltersBuildQuery = () => {
return this.dynamicFilters.map((filter) => {
return filter.buildQuery();
});
};
/**
* Retrieve dynamic filter roles.
* @returns {IFilterRole[]}
*/
private dynamicFilterTableColumns = (): IFilterRole[] => {
const localFilterRoles = [];
this.dynamicFilters.forEach((dynamicFilter) => {
const { filterRoles } = dynamicFilter;
localFilterRoles.push(
...(Array.isArray(filterRoles) ? filterRoles : [filterRoles]),
);
});
return localFilterRoles;
};
/**
* Builds queries of filter roles.
*/
public buildQuery = () => {
const buildersCallbacks = this.dynamicFiltersBuildQuery();
const tableColumns = this.dynamicFilterTableColumns();
return (builder) => {
buildersCallbacks.forEach((builderCallback) => {
builderCallback(builder);
});
this.buildFilterRolesJoins(builder);
};
};
/**
* Retrieve response metadata from all filters adapters.
*/
public getResponseMeta = () => {
const responseMeta = {};
this.dynamicFilters.forEach((filter) => {
const { responseMeta: filterMeta } = filter;
forEach(filterMeta, (value, key) => {
responseMeta[key] = value;
});
});
return responseMeta;
};
}

View File

@@ -0,0 +1,40 @@
import { BaseModel } from '@/models/Model';
// import { IModel, ISortOrder } from "./Model";
export type ISortOrder = 'DESC' | 'ASC';
export interface IDynamicFilter {
setModel(model: BaseModel): void;
buildQuery(): void;
getResponseMeta();
}
export interface IFilterRole {
fieldKey: string;
value: string;
condition?: string;
index?: number;
comparator?: string;
}
export interface IDynamicListFilter {
customViewId?: number;
filterRoles?: IFilterRole[];
columnSortBy: ISortOrder;
sortOrder: string;
stringifiedFilterRoles: string;
searchKeyword?: string;
viewSlug?: string;
}
export interface IDynamicListService {
dynamicList(
model: any,
filter: IDynamicListFilter,
): Promise<any>;
handlerErrorsToResponse(error, req, res, next): void;
}
// Search role.
export interface ISearchRole {
fieldKey: string;
comparator: string;
}

View File

@@ -0,0 +1,55 @@
import { BaseModel } from '@/models/Model';
import { IDynamicFilter } from './DynamicFilter.types';
export class DynamicFilterAbstractor {
model: BaseModel;
dynamicFilters: IDynamicFilter[];
/**
* Extract relation table name from relation.
* @param {String} column -
* @return {String} - join relation table.
*/
protected getTableFromRelationColumn = (column: string) => {
const splitedColumn = column.split('.');
return splitedColumn.length > 0 ? splitedColumn[0] : '';
};
/**
* Builds view roles join queries.
* @param {String} tableName - Table name.
* @param {Array} roles - Roles.
*/
protected buildFilterRolesJoins = (builder) => {
this.dynamicFilters.forEach((dynamicFilter) => {
const relationsFields = dynamicFilter.relationFields;
this.buildFieldsJoinQueries(builder, relationsFields);
});
};
/**
* Builds join queries of fields.
* @param builder -
* @param {string[]} fieldsRelations -
*/
private buildFieldsJoinQueries = (builder, fieldsRelations: string[]) => {
fieldsRelations.forEach((fieldRelation) => {
const relation = this.model.relationMappings[fieldRelation];
if (relation) {
const splitToRelation = relation.join.to.split('.');
const relationTable = splitToRelation[0] || '';
builder.join(relationTable, relation.join.from, '=', relation.join.to);
}
});
};
/**
* Retrieve the dynamic filter mode.
*/
protected getModel() {
return this.model;
}
}

View File

@@ -0,0 +1,27 @@
import { IFilterRole } from './DynamicFilter.types';
import { DynamicFilterFilterRoles } from './DynamicFilterFilterRoles';
export class DynamicFilterAdvancedFilter extends DynamicFilterFilterRoles {
private filterRoles: IFilterRole[];
/**
* Constructor method.
* @param {Array} filterRoles -
* @param {Array} resourceFields -
*/
constructor(filterRoles: IFilterRole[]) {
super();
this.filterRoles = filterRoles;
this.setResponseMeta();
}
/**
* Sets response meta.
*/
private setResponseMeta() {
this.responseMeta = {
filterRoles: this.filterRoles,
};
}
}

View File

@@ -0,0 +1,51 @@
import { DynamicFilterAbstractor } from './DynamicFilterRoleAbstractor';
import { IFilterRole } from '@/interfaces';
export class DynamicFilterFilterRoles extends DynamicFilterAbstractor {
private filterRoles: IFilterRole[];
/**
* On initialize filter roles.
*/
public onInitialize() {
super.onInitialize();
this.setFilterRolesRelations();
}
/**
* Builds filter roles logic expression.
* @return {string}
*/
private buildLogicExpression(): string {
let expression = '';
this.filterRoles.forEach((role, index) => {
expression +=
index === 0 ? `${role.index} ` : `${role.condition} ${role.index} `;
});
return expression.trim();
}
/**
* Builds database query of view roles.
*/
protected buildQuery() {
const logicExpression = this.buildLogicExpression();
return (builder) => {
this.buildFilterQuery(
this.model,
this.filterRoles,
logicExpression
)(builder);
};
}
/**
* Sets filter roles relations if field was relation type.
*/
private setFilterRolesRelations() {
this.filterRoles.forEach((relationRole) => {
this.setRelationIfRelationField(relationRole.fieldKey);
});
}
}

View File

@@ -0,0 +1,72 @@
import { OPERATION } from '@/libs/logic-evaluation/Parser';
export default class QueryParser {
constructor(tree, queries) {
this.tree = tree;
this.queries = queries;
this.query = null;
}
setQuery(query) {
this.query = query.clone();
}
parse() {
return this.parseNode(this.tree);
}
parseNode(node) {
if (typeof node === 'string') {
const nodeQuery = this.getQuery(node);
return (query) => {
nodeQuery(query);
};
}
if (OPERATION[node.operation] === undefined) {
throw new Error(`unknow expression ${node.operation}`);
}
const leftQuery = this.getQuery(node.left);
const rightQuery = this.getQuery(node.right);
switch (node.operation) {
case '&&':
case 'AND':
default:
return (nodeQuery) =>
nodeQuery.where((query) => {
query.where((q) => {
leftQuery(q);
});
query.andWhere((q) => {
rightQuery(q);
});
});
case '||':
case 'OR':
return (nodeQuery) =>
nodeQuery.where((query) => {
query.where((q) => {
leftQuery(q);
});
query.orWhere((q) => {
rightQuery(q);
});
});
}
}
getQuery(node) {
if (typeof node !== 'string' && node !== null) {
return this.parseNode(node);
}
const value = parseFloat(node);
if (!isNaN(value)) {
if (typeof this.queries[node] === 'undefined') {
throw new Error(`unknow query under index ${node}`);
}
return this.queries[node];
}
return null;
}
}

View File

@@ -0,0 +1,388 @@
import moment from 'moment';
import * as R from 'ramda';
import { IFilterRole, IDynamicFilter, } from './DynamicFilter.types';
import Parser from '@/libs/logic-evaluation/Parser';
import { Lexer } from '@/libs/logic-evaluation/Lexer';
import DynamicFilterQueryParser from './DynamicFilterQueryParser';
import { COMPARATOR_TYPE, FIELD_TYPE } from './constants';
import { BaseModel } from '@/models/Model';
export abstract class DynamicFilterAbstractor
implements IDynamicFilter
{
protected filterRoles: IFilterRole[] = [];
protected tableName: string;
protected model: BaseModel;
protected responseMeta: { [key: string]: any } = {};
public relationFields = [];
/**
* Sets model the dynamic filter service.
* @param {IModel} model
*/
public setModel(model: BaseModel) {
this.model = model;
this.tableName = model.tableName;
}
/**
* Transformes filter roles to map by index.
* @param {IModel} model
* @param {IFilterRole[]} roles
* @returns
*/
protected convertRolesMapByIndex = (model, roles) => {
const rolesIndexSet = {};
roles.forEach((role) => {
rolesIndexSet[role.index] = this.buildRoleQuery(model, role);
});
return rolesIndexSet;
};
/**
* Builds database query from stored view roles.
* @param {Array} roles -
* @return {Function}
*/
protected buildFilterRolesQuery = (
model: IModel,
roles: IFilterRole[],
logicExpression: string = ''
) => {
const rolesIndexSet = this.convertRolesMapByIndex(model, roles);
// Lexer for logic expression.
const lexer = new Lexer(logicExpression);
const tokens = lexer.getTokens();
// Parse the logic expression.
const parser = new Parser(tokens);
const parsedTree = parser.parse();
const queryParser = new DynamicFilterQueryParser(parsedTree, rolesIndexSet);
return queryParser.parse();
};
/**
* Parses the logic expression to base expression.
* @param {string} logicExpression -
* @return {string}
*/
private parseLogicExpression(logicExpression: string): string {
return R.compose(
R.replace(/or|OR/g, '||'),
R.replace(/and|AND/g, '&&'),
)(logicExpression);
}
/**
* Builds filter query for query builder.
* @param {String} tableName - Table name.
* @param {Array} roles - Filter roles.
* @param {String} logicExpression - Logic expression.
*/
protected buildFilterQuery = (
model: IModel,
roles: IFilterRole[],
logicExpression: string
) => {
const basicExpression = this.parseLogicExpression(logicExpression);
return (builder) => {
this.buildFilterRolesQuery(model, roles, basicExpression)(builder);
};
};
/**
* Retrieve relation column of comparator fieldز
*/
private getFieldComparatorRelationColumn(field) {
const relation = this.model.relationMappings[field.relationKey];
if (relation) {
const relationModel = relation.modelClass;
const relationColumn =
field.relationEntityKey === 'id'
? 'id'
: relationModel.getField(field.relationEntityKey, 'column');
return `${relationModel.tableName}.${relationColumn}`;
}
}
/**
* Retrieve the comparator field column.
* @param {IModel} model -
* @param {} -
*/
private getFieldComparatorColumn = (field) => {
return field.fieldType === FIELD_TYPE.RELATION
? this.getFieldComparatorRelationColumn(field)
: `${this.tableName}.${field.column}`;
};
/**
* Builds roles queries.
* @param {IModel} model -
* @param {Object} role -
*/
protected buildRoleQuery = (model: BaseModel, role: IFilterRole) => {
const field = model.getField(role.fieldKey);
const comparatorColumn = this.getFieldComparatorColumn(field);
// Field relation custom query.
if (typeof field.filterCustomQuery !== 'undefined') {
return (builder) => {
field.filterCustomQuery(builder, role);
};
}
switch (field.fieldType) {
case FIELD_TYPE.BOOLEAN:
case FIELD_TYPE.ENUMERATION:
return this.booleanRoleQueryBuilder(role, comparatorColumn);
case FIELD_TYPE.NUMBER:
return this.numberRoleQueryBuilder(role, comparatorColumn);
case FIELD_TYPE.DATE:
return this.dateQueryBuilder(role, comparatorColumn);
case FIELD_TYPE.TEXT:
default:
return this.textRoleQueryBuilder(role, comparatorColumn);
}
};
/**
* Boolean column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns
*/
protected booleanRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUALS:
case COMPARATOR_TYPE.EQUAL:
case COMPARATOR_TYPE.IS:
default:
return (builder) => {
builder.where(comparatorColumn, '=', role.value);
};
case COMPARATOR_TYPE.NOT_EQUAL:
case COMPARATOR_TYPE.NOT_EQUALS:
case COMPARATOR_TYPE.IS_NOT:
return (builder) => {
builder.where(comparatorColumn, '<>', role.value);
};
}
};
/**
* Numeric column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns
*/
protected numberRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUALS:
case COMPARATOR_TYPE.EQUAL:
default:
return (builder) => {
builder.where(comparatorColumn, '=', role.value);
};
case COMPARATOR_TYPE.NOT_EQUAL:
case COMPARATOR_TYPE.NOT_EQUALS:
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case COMPARATOR_TYPE.BIGGER_THAN:
case COMPARATOR_TYPE.BIGGER:
return (builder) => {
builder.where(comparatorColumn, '>', role.value);
};
case COMPARATOR_TYPE.BIGGER_OR_EQUALS:
return (builder) => {
builder.where(comparatorColumn, '>=', role.value);
};
case COMPARATOR_TYPE.SMALLER_THAN:
case COMPARATOR_TYPE.SMALLER:
return (builder) => {
builder.where(comparatorColumn, '<', role.value);
};
case COMPARATOR_TYPE.SMALLER_OR_EQUALS:
return (builder) => {
builder.where(comparatorColumn, '<=', role.value);
};
}
};
/**
* Text column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns {Function}
*/
protected textRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUAL:
case COMPARATOR_TYPE.EQUALS:
case COMPARATOR_TYPE.IS:
default:
return (builder) => {
builder.where(comparatorColumn, role.value);
};
case COMPARATOR_TYPE.NOT_EQUALS:
case COMPARATOR_TYPE.NOT_EQUAL:
case COMPARATOR_TYPE.IS_NOT:
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case COMPARATOR_TYPE.CONTAIN:
case COMPARATOR_TYPE.CONTAINS:
return (builder) => {
builder.where(comparatorColumn, 'LIKE', `%${role.value}%`);
};
case COMPARATOR_TYPE.NOT_CONTAIN:
case COMPARATOR_TYPE.NOT_CONTAINS:
return (builder) => {
builder.whereNot(comparatorColumn, 'LIKE', `%${role.value}%`);
};
case COMPARATOR_TYPE.STARTS_WITH:
case COMPARATOR_TYPE.START_WITH:
return (builder) => {
builder.where(comparatorColumn, 'LIKE', `${role.value}%`);
};
case COMPARATOR_TYPE.ENDS_WITH:
case COMPARATOR_TYPE.END_WITH:
return (builder) => {
builder.where(comparatorColumn, 'LIKE', `%${role.value}`);
};
}
};
/**
* Date column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns {Function}
*/
protected dateQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.AFTER:
case COMPARATOR_TYPE.BEFORE:
return (builder) => {
this.dateQueryAfterBeforeComparator(role, comparatorColumn, builder);
};
case COMPARATOR_TYPE.IN:
return (builder) => {
this.dateQueryInComparator(role, comparatorColumn, builder);
};
}
};
/**
* Date query 'IN' comparator type.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @param builder
*/
protected dateQueryInComparator = (
role: IFilterRole,
comparatorColumn: string,
builder
) => {
const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
).isValid();
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (hasTimeFormat) {
const targetDateTime = moment(role.value).format(dateFormat);
builder.where(comparatorColumn, '=', targetDateTime);
} else {
const startDate = moment(role.value).startOf('day');
const endDate = moment(role.value).endOf('day');
builder.where(comparatorColumn, '>=', startDate.format(dateFormat));
builder.where(comparatorColumn, '<=', endDate.format(dateFormat));
}
};
/**
* Date query after/before comparator type.
* @param {IFilterRole} role
* @param {string} comparatorColumn - Column.
* @param builder
*/
protected dateQueryAfterBeforeComparator = (
role: IFilterRole,
comparatorColumn: string,
builder
) => {
const comparator = role.comparator === COMPARATOR_TYPE.BEFORE ? '<' : '>';
const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
).isValid();
const targetDate = moment(role.value);
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (!hasTimeFormat) {
if (role.comparator === COMPARATOR_TYPE.BEFORE) {
targetDate.startOf('day');
} else {
targetDate.endOf('day');
}
}
const comparatorValue = targetDate.format(dateFormat);
builder.where(comparatorColumn, comparator, comparatorValue);
};
/**
* Registers relation field if the given field was relation type
* and not registered.
* @param {string} fieldKey - Field key.
*/
protected setRelationIfRelationField = (fieldKey: string): void => {
const field = this.model.getField(fieldKey);
const isAlreadyRegistered = this.relationFields.some(
(field) => field === fieldKey
);
if (
!isAlreadyRegistered &&
field &&
field.fieldType === FIELD_TYPE.RELATION
) {
this.relationFields.push(field.relationKey);
}
};
/**
* Retrieve the model.
*/
getModel() {
return this.model;
}
/**
* On initialize the registered dynamic filter.
*/
onInitialize() {}
}

View File

@@ -0,0 +1,48 @@
import { IFilterRole } from './DynamicFilter.types';
import { DynamicFilterFilterRoles } from './DynamicFilterFilterRoles';
export class DynamicFilterSearch extends DynamicFilterFilterRoles {
private searchKeyword: string;
private filterRoles: IFilterRole[];
/**
* Constructor method.
* @param {string} searchKeyword - Search keyword.
*/
constructor(searchKeyword: string) {
super();
this.searchKeyword = searchKeyword;
}
/**
* On initialize the dynamic filter.
*/
public onInitialize() {
super.onInitialize();
this.filterRoles = this.getModelSearchFilterRoles(this.searchKeyword);
}
/**
* Retrieve the filter roles from model search roles.
* @param {string} searchKeyword
* @returns {IFilterRole[]}
*/
private getModelSearchFilterRoles(searchKeyword: string): IFilterRole[] {
const model = this.getModel();
return model.searchRoles.map((searchRole, index) => ({
...searchRole,
value: searchKeyword,
index: index + 1,
}));
}
/**
*
*/
setResponseMeta() {
this.responseMeta = {
searchKeyword: this.searchKeyword,
};
}
}

View File

@@ -0,0 +1,92 @@
import { FIELD_TYPE } from './constants';
import { DynamicFilterAbstractor } from './DynamicFilterAbstractor';
interface ISortRole {
fieldKey: string;
order: string;
}
export class DynamicFilterSortBy extends DynamicFilterAbstractor {
private sortRole: ISortRole = {};
/**
* Constructor method.
* @param {string} sortByFieldKey
* @param {string} sortDirection
*/
constructor(sortByFieldKey: string, sortDirection: string) {
super();
this.sortRole = {
fieldKey: sortByFieldKey,
order: sortDirection,
};
this.setResponseMeta();
}
/**
* On initialize the dyanmic sort by.
*/
public onInitialize() {
this.setRelationIfRelationField(this.sortRole.fieldKey);
}
/**
* Retrieve field comparator relatin column.
* @param field
* @returns {string}
*/
private getFieldComparatorRelationColumn = (field): string => {
const relation = this.model.relationMappings[field.relationKey];
if (relation) {
const relationModel = relation.modelClass;
const relationField = relationModel.getField(field.relationEntityLabel);
return `${relationModel.tableName}.${relationField.column}`;
}
return '';
};
/**
* Retrieve the comparator field column.
* @param {IModel} field
* @returns {string}
*/
private getFieldComparatorColumn = (field): string => {
return field.fieldType === FIELD_TYPE.RELATION
? this.getFieldComparatorRelationColumn(field)
: `${this.tableName}.${field.column}`;
};
/**
* Builds database query of sort by column on the given direction.
*/
public buildQuery = () => {
const field = this.model.getField(this.sortRole.fieldKey);
const comparatorColumn = this.getFieldComparatorColumn(field);
// Sort custom query.
if (typeof field.sortCustomQuery !== 'undefined') {
return (builder) => {
field.sortCustomQuery(builder, this.sortRole);
};
}
return (builder) => {
if (this.sortRole.fieldKey) {
builder.orderBy(`${comparatorColumn}`, this.sortRole.order);
}
};
};
/**
* Sets response meta.
*/
public setResponseMeta() {
this.responseMeta = {
sortOrder: this.sortRole.fieldKey,
sortBy: this.sortRole.order,
};
}
}

View File

@@ -0,0 +1,56 @@
import { omit } from 'lodash';
import { IView, IViewRole } from '@/interfaces';
import { DynamicFilterAbstractor } from './DynamicFilterAbstractor';
export class DynamicFilterViews extends DynamicFilterAbstractor {
private viewSlug: string;
private logicExpression: string;
private filterRoles: IViewRole[];
private viewColumns = [];
/**
* Constructor method.
* @param {IView} view -
*/
constructor(view: IView) {
super();
this.viewSlug = view.slug;
this.filterRoles = view.roles;
this.viewColumns = view.columns;
this.logicExpression = view.rolesLogicExpression
.replace('AND', '&&')
.replace('OR', '||');
this.setResponseMeta();
}
/**
* Builds database query of view roles.
*/
public buildQuery() {
return (builder) => {
this.buildFilterQuery(
this.model,
this.filterRoles,
this.logicExpression
)(builder);
};
}
/**
* Sets response meta.
*/
public setResponseMeta() {
this.responseMeta = {
view: {
logicExpression: this.logicExpression,
filterRoles: this.filterRoles.map((filterRole) => ({
...omit(filterRole, ['id', 'viewId']),
})),
viewSlug: this.viewSlug,
viewColumns: this.viewColumns,
},
};
}
}

View File

@@ -0,0 +1,43 @@
export const COMPARATOR_TYPE = {
EQUAL: 'equal',
EQUALS: 'equals',
NOT_EQUAL: 'not_equal',
NOT_EQUALS: 'not_equals',
BIGGER_THAN: 'bigger_than',
BIGGER: 'bigger',
BIGGER_OR_EQUALS: 'bigger_or_equals',
SMALLER_THAN: 'smaller_than',
SMALLER: 'smaller',
SMALLER_OR_EQUALS: 'smaller_or_equals',
IS: 'is',
IS_NOT: 'is_not',
CONTAINS: 'contains',
CONTAIN: 'contain',
NOT_CONTAINS: 'contains',
NOT_CONTAIN: 'contain',
AFTER: 'after',
BEFORE: 'before',
IN: 'in',
STARTS_WITH: 'starts_with',
START_WITH: 'start_with',
ENDS_WITH: 'ends_with',
END_WITH: 'end_with'
};
export const FIELD_TYPE = {
TEXT: 'text',
NUMBER: 'number',
ENUMERATION: 'enumeration',
BOOLEAN: 'boolean',
RELATION: 'relation',
DATE: 'date',
COMPUTED: 'computed'
};

View File

@@ -0,0 +1,11 @@
import { DynamicFilter } from './DynamicFilter';
import { DynamicFilterSortBy } from './DynamicFilterSortBy';
import { DynamicFilterViews } from './DynamicFilterViews';
import { DynamicFilterFilterRoles } from './DynamicFilterFilterRoles';
export {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
};

View File

@@ -0,0 +1 @@
export class DynamicListAbstract {}

View File

@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { DynamicListAbstract } from './DynamicListAbstract';
import { ERRORS } from './constants';
import { DynamicFilterViews } from './DynamicFilter';
import { ServiceError } from '../Items/ServiceError';
import { BaseModel } from '@/models/Model';
@Injectable()
export class DynamicListCustomView extends DynamicListAbstract {
/**
* Retreive custom view or throws error not found.
* @param {number} tenantId
* @param {number} viewId
* @return {Promise<IView>}
*/
private getCustomViewOrThrowError = async (
viewSlug: string,
model: BaseModel,
) => {
// Finds the default view by the given view slug.
const defaultView = model.getDefaultViewBySlug(viewSlug);
if (!defaultView) {
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
}
return defaultView;
};
/**
* Dynamic list custom view.
* @param {IModel} model
* @param {number} customViewId
* @returns
*/
public dynamicListCustomView = async (
dynamicFilter: any,
customViewSlug: string,
) => {
const model = dynamicFilter.getModel();
// Retrieve the custom view or throw not found.
const view = await this.getCustomViewOrThrowError(customViewSlug, model);
return new DynamicFilterViews(view);
};
}

View File

@@ -0,0 +1,104 @@
import * as R from 'ramda';
import { Injectable } from '@nestjs/common';
import validator from 'is-my-json-valid';
import { IFilterRole } from './DynamicFilter/DynamicFilter.types';
import { DynamicListAbstract } from './DynamicListAbstract';
import { DynamicFilterAdvancedFilter } from './DynamicFilter/DynamicFilterAdvancedFilter';
import { ERRORS } from './constants';
import { ServiceError } from '../Items/ServiceError';
import { BaseModel } from '@/models/Model';
@Injectable()
export class DynamicListFilterRoles extends DynamicListAbstract {
/**
* Validates filter roles schema.
* @param {IFilterRole[]} filterRoles - Filter roles.
*/
private validateFilterRolesSchema = (filterRoles: IFilterRole[]) => {
const validate = validator({
required: ['fieldKey', 'value'],
type: 'object',
properties: {
condition: { type: 'string' },
fieldKey: { type: 'string' },
value: { type: 'string' },
},
});
const invalidFields = filterRoles.filter((filterRole) => {
return !validate(filterRole);
});
if (invalidFields.length > 0) {
throw new ServiceError(ERRORS.STRINGIFIED_FILTER_ROLES_INVALID);
}
};
/**
* Retrieve filter roles fields key that not exists on the given model.
* @param {BaseModel} model
* @param {IFilterRole} filterRoles
* @returns {string[]}
*/
private getFilterRolesFieldsNotExist = (
model: BaseModel,
filterRoles: IFilterRole[],
): string[] => {
return filterRoles
.filter((filterRole) => !model.getField(filterRole.fieldKey))
.map((filterRole) => filterRole.fieldKey);
};
/**
* Validates existance the fields of filter roles.
* @param {BaseModel} model
* @param {IFilterRole[]} filterRoles
* @throws {ServiceError}
*/
private validateFilterRolesFieldsExistance = (
model: BaseModel,
filterRoles: IFilterRole[],
) => {
const invalidFieldsKeys = this.getFilterRolesFieldsNotExist(
model,
filterRoles,
);
if (invalidFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
}
};
/**
* Associate index to filter roles.
* @param {IFilterRole[]} filterRoles
* @returns {IFilterRole[]}
*/
private incrementFilterRolesIndex = (
filterRoles: IFilterRole[],
): IFilterRole[] => {
return filterRoles.map((filterRole, index) => ({
...filterRole,
index: index + 1,
}));
};
/**
* Dynamic list filter roles.
* @param {BaseModel} model
* @param {IFilterRole[]} filterRoles
* @returns {DynamicFilterFilterRoles}
*/
public dynamicList = (
model: BaseModel,
filterRoles: IFilterRole[],
): DynamicFilterAdvancedFilter => {
const filterRolesParsed = R.compose(this.incrementFilterRolesIndex)(
filterRoles,
);
// Validate filter roles json schema.
this.validateFilterRolesSchema(filterRolesParsed);
// Validate the model resource fields.
this.validateFilterRolesFieldsExistance(model, filterRoles);
return new DynamicFilterAdvancedFilter(filterRolesParsed);
};
}

View File

@@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common';
import { DynamicListAbstract } from './DynamicListAbstract';
import { DynamicFilterSearch } from './DynamicFilter/DynamicFilterSearch';
@Injectable()
export class DynamicListSearch extends DynamicListAbstract {
/**
* Dynamic list filter roles.
* @param {string} searchKeyword - Search keyword.
* @returns {DynamicFilterFilterRoles}
*/
public dynamicSearch = (searchKeyword: string) => {
return new DynamicFilterSearch(searchKeyword);
};
}

View File

@@ -0,0 +1,101 @@
import { castArray, isEmpty } from 'lodash';
import {
IDynamicListFilter,
IDynamicListService,
} from './DynamicFilter/DynamicFilter.types';
import { DynamicListSortBy } from './DynamicListSortBy';
import { DynamicListSearch } from './DynamicListSearch';
import { DynamicListCustomView } from './DynamicListCustomView';
import { Injectable } from '@nestjs/common';
import { DynamicListFilterRoles } from './DynamicListFilterRoles';
import { DynamicFilter } from './DynamicFilter';
import { BaseModel } from '@/models/Model';
@Injectable()
export class DynamicListService implements IDynamicListService {
constructor(
private dynamicListFilterRoles: DynamicListFilterRoles,
private dynamicListSearch: DynamicListSearch,
private dynamicListSortBy: DynamicListSortBy,
private dynamicListView: DynamicListCustomView,
) {}
/**
* Parses filter DTO.
* @param {IMode} model -
* @param {} filterDTO -
*/
private parseFilterObject = (model, filterDTO) => {
return {
// Merges the default properties with filter object.
...(model.defaultSort
? {
sortOrder: model.defaultSort.sortOrder,
columnSortBy: model.defaultSort.sortOrder,
}
: {}),
...filterDTO,
};
};
/**
* Dynamic listing.
* @param {number} tenantId - Tenant id.
* @param {IModel} model - Model.
* @param {IDynamicListFilter} filter - Dynamic filter DTO.
*/
public dynamicList = async (model: BaseModel, filter: IDynamicListFilter) => {
const dynamicFilter = new DynamicFilter(model);
// Parses the filter object.
const parsedFilter = this.parseFilterObject(model, filter);
// Search by keyword.
if (filter.searchKeyword) {
const dynamicListSearch = this.dynamicListSearch.dynamicSearch(
filter.searchKeyword,
);
dynamicFilter.setFilter(dynamicListSearch);
}
// Custom view filter roles.
if (filter.viewSlug) {
const dynamicListCustomView =
await this.dynamicListView.dynamicListCustomView(
dynamicFilter,
filter.viewSlug,
);
dynamicFilter.setFilter(dynamicListCustomView);
}
// Sort by the given column.
if (parsedFilter.columnSortBy) {
const dynmaicListSortBy = this.dynamicListSortBy.dynamicSortBy(
model,
parsedFilter.columnSortBy,
parsedFilter.sortOrder,
);
dynamicFilter.setFilter(dynmaicListSortBy);
}
// Filter roles.
if (!isEmpty(parsedFilter.filterRoles)) {
const dynamicFilterRoles = this.dynamicListFilterRoles.dynamicList(
model,
parsedFilter.filterRoles,
);
dynamicFilter.setFilter(dynamicFilterRoles);
}
return dynamicFilter;
};
/**
* Parses stringified filter roles.
* @param {string} stringifiedFilterRoles - Stringified filter roles.
*/
public parseStringifiedFilter = (filterRoles: IDynamicListFilter) => {
return {
...filterRoles,
filterRoles: filterRoles.stringifiedFilterRoles
? castArray(JSON.parse(filterRoles.stringifiedFilterRoles))
: [],
};
};
}

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { DynamicListAbstract } from './DynamicListAbstract';
import { ISortOrder } from './DynamicFilter/DynamicFilter.types';
import { ERRORS } from './constants';
import { DynamicFilterSortBy } from './DynamicFilter';
import { ServiceError } from '../Items/ServiceError';
import { BaseModel } from '@/models/Model';
@Injectable()
export class DynamicListSortBy extends DynamicListAbstract {
/**
* Dynamic list sort by.
* @param {BaseModel} model
* @param {string} columnSortBy
* @param {ISortOrder} sortOrder
* @returns {DynamicFilterSortBy}
*/
public dynamicSortBy(
model: BaseModel,
columnSortBy: string,
sortOrder: ISortOrder,
) {
this.validateSortColumnExistance(model, columnSortBy);
return new DynamicFilterSortBy(columnSortBy, sortOrder);
}
/**
* Validates the sort column whether exists.
* @param {IModel} model - Model.
* @param {string} columnSortBy - Sort column
* @throws {ServiceError}
*/
private validateSortColumnExistance(model: any, columnSortBy: string) {
const field = model.getField(columnSortBy);
if (!field) {
throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND);
}
}
}

View File

@@ -0,0 +1,6 @@
export const ERRORS = {
STRINGIFIED_FILTER_ROLES_INVALID: 'stringified_filter_roles_invalid',
VIEW_NOT_FOUND: 'view_not_found',
SORT_COLUMN_NOT_FOUND: 'sort_column_not_found',
FILTER_ROLES_FIELDS_NOT_FOUND: 'filter_roles_fields_not_found',
};