refactor: dynamic list to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-01-12 18:22:48 +02:00
parent ddaea20d16
commit 270b421a6c
117 changed files with 4232 additions and 1493 deletions

View File

@@ -1,17 +1,15 @@
import { forEach } from 'lodash';
import { DynamicFilterAbstractor } from './DynamicFilterAbstractor';
import { IDynamicFilter, IFilterRole, IModel } from '@/interfaces';
import { IDynamicFilter, IFilterRole } from './DynamicFilter.types';
import { BaseModel } from '@/models/Model';
import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor';
export class DynamicFilter extends DynamicFilterAbstractor {
private model: BaseModel;
private dynamicFilters: IDynamicFilter[];
/**
* Constructor.
* @param {String} tableName -
*/
constructor(model: BaseModel) {
constructor(model: typeof BaseModel) {
super();
this.model = model;
@@ -22,7 +20,7 @@ export class DynamicFilter extends DynamicFilterAbstractor {
* Registers the given dynamic filter.
* @param {IDynamicFilter} filterRole - Filter role.
*/
public setFilter = (dynamicFilter: IDynamicFilter) => {
public setFilter = (dynamicFilter: DynamicFilterRoleAbstractor) => {
dynamicFilter.setModel(this.model);
dynamicFilter.onInitialize();

View File

@@ -1,10 +1,10 @@
import { BaseModel } from '@/models/Model';
// import { IModel, ISortOrder } from "./Model";
export type ISortOrder = 'DESC' | 'ASC';
export interface IDynamicFilter {
setModel(model: BaseModel): void;
setModel(model: typeof BaseModel): void;
onInitialize(): void;
buildQuery(): void;
getResponseMeta();
}
@@ -20,7 +20,7 @@ export interface IDynamicListFilter {
filterRoles?: IFilterRole[];
columnSortBy: ISortOrder;
sortOrder: string;
stringifiedFilterRoles: string;
stringifiedFilterRoles?: string;
searchKeyword?: string;
viewSlug?: string;
}

View File

@@ -2,8 +2,8 @@ import { BaseModel } from '@/models/Model';
import { IDynamicFilter } from './DynamicFilter.types';
export class DynamicFilterAbstractor {
model: BaseModel;
dynamicFilters: IDynamicFilter[];
public model: typeof BaseModel;
public dynamicFilters: IDynamicFilter[];
/**
* Extract relation table name from relation.

View File

@@ -2,8 +2,6 @@ import { IFilterRole } from './DynamicFilter.types';
import { DynamicFilterFilterRoles } from './DynamicFilterFilterRoles';
export class DynamicFilterAdvancedFilter extends DynamicFilterFilterRoles {
private filterRoles: IFilterRole[];
/**
* Constructor method.
* @param {Array} filterRoles -

View File

@@ -1,8 +1,6 @@
import { DynamicFilterAbstractor } from './DynamicFilterRoleAbstractor';
import { IFilterRole } from '@/interfaces';
import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor';
export class DynamicFilterFilterRoles extends DynamicFilterAbstractor {
private filterRoles: IFilterRole[];
export class DynamicFilterFilterRoles extends DynamicFilterRoleAbstractor {
/**
* On initialize filter roles.
*/
@@ -28,14 +26,14 @@ export class DynamicFilterFilterRoles extends DynamicFilterAbstractor {
/**
* Builds database query of view roles.
*/
protected buildQuery() {
public buildQuery() {
const logicExpression = this.buildLogicExpression();
return (builder) => {
this.buildFilterQuery(
this.model,
this.filterRoles,
logicExpression
logicExpression,
)(builder);
};
}

View File

@@ -1,18 +1,19 @@
import moment from 'moment';
import * as R from 'ramda';
import { IFilterRole, IDynamicFilter, } from './DynamicFilter.types';
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';
import { IMetadataModel } from '../models/MetadataModel';
export abstract class DynamicFilterAbstractor
implements IDynamicFilter
{
type MetadataModel = typeof BaseModel & IMetadataModel;
export abstract class DynamicFilterRoleAbstractor implements IDynamicFilter {
protected filterRoles: IFilterRole[] = [];
protected tableName: string;
protected model: BaseModel;
protected model: MetadataModel;
protected responseMeta: { [key: string]: any } = {};
public relationFields = [];
@@ -20,7 +21,7 @@ export abstract class DynamicFilterAbstractor
* Sets model the dynamic filter service.
* @param {IModel} model
*/
public setModel(model: BaseModel) {
public setModel(model: MetadataModel) {
this.model = model;
this.tableName = model.tableName;
}
@@ -46,9 +47,9 @@ export abstract class DynamicFilterAbstractor
* @return {Function}
*/
protected buildFilterRolesQuery = (
model: IModel,
model: typeof BaseModel,
roles: IFilterRole[],
logicExpression: string = ''
logicExpression: string = '',
) => {
const rolesIndexSet = this.convertRolesMapByIndex(model, roles);
@@ -67,7 +68,7 @@ export abstract class DynamicFilterAbstractor
/**
* Parses the logic expression to base expression.
* @param {string} logicExpression -
* @param {string} logicExpression -
* @return {string}
*/
private parseLogicExpression(logicExpression: string): string {
@@ -84,9 +85,9 @@ export abstract class DynamicFilterAbstractor
* @param {String} logicExpression - Logic expression.
*/
protected buildFilterQuery = (
model: IModel,
model: typeof BaseModel,
roles: IFilterRole[],
logicExpression: string
logicExpression: string,
) => {
const basicExpression = this.parseLogicExpression(logicExpression);
@@ -98,7 +99,7 @@ export abstract class DynamicFilterAbstractor
/**
* Retrieve relation column of comparator fieldز
*/
private getFieldComparatorRelationColumn(field) {
protected getFieldComparatorRelationColumn(field: any): string {
const relation = this.model.relationMappings[field.relationKey];
if (relation) {
@@ -128,7 +129,7 @@ export abstract class DynamicFilterAbstractor
* @param {IModel} model -
* @param {Object} role -
*/
protected buildRoleQuery = (model: BaseModel, role: IFilterRole) => {
protected buildRoleQuery = (model: MetadataModel, role: IFilterRole) => {
const field = model.getField(role.fieldKey);
const comparatorColumn = this.getFieldComparatorColumn(field);
@@ -160,7 +161,7 @@ export abstract class DynamicFilterAbstractor
*/
protected booleanRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
comparatorColumn: string,
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUALS:
@@ -187,7 +188,7 @@ export abstract class DynamicFilterAbstractor
*/
protected numberRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
comparatorColumn: string,
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUALS:
@@ -230,7 +231,7 @@ export abstract class DynamicFilterAbstractor
*/
protected textRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
comparatorColumn: string,
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUAL:
@@ -266,7 +267,6 @@ export abstract class DynamicFilterAbstractor
return (builder) => {
builder.where(comparatorColumn, 'LIKE', `%${role.value}`);
};
}
};
@@ -278,7 +278,7 @@ export abstract class DynamicFilterAbstractor
*/
protected dateQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
comparatorColumn: string,
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.AFTER:
@@ -302,12 +302,12 @@ export abstract class DynamicFilterAbstractor
protected dateQueryInComparator = (
role: IFilterRole,
comparatorColumn: string,
builder
builder,
) => {
const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
true,
).isValid();
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
@@ -332,13 +332,13 @@ export abstract class DynamicFilterAbstractor
protected dateQueryAfterBeforeComparator = (
role: IFilterRole,
comparatorColumn: string,
builder
builder,
) => {
const comparator = role.comparator === COMPARATOR_TYPE.BEFORE ? '<' : '>';
const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
true,
).isValid();
const targetDate = moment(role.value);
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
@@ -355,16 +355,14 @@ export abstract class DynamicFilterAbstractor
};
/**
* Registers relation field if the given field was relation type
* and not registered.
* 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
(field) => field === fieldKey,
);
if (
!isAlreadyRegistered &&
field &&
@@ -385,4 +383,11 @@ export abstract class DynamicFilterAbstractor
* On initialize the registered dynamic filter.
*/
onInitialize() {}
buildQuery(): void {
throw new Error('Method not implemented.');
}
getResponseMeta() {
throw new Error('Method not implemented.');
}
}

View File

@@ -3,7 +3,6 @@ import { DynamicFilterFilterRoles } from './DynamicFilterFilterRoles';
export class DynamicFilterSearch extends DynamicFilterFilterRoles {
private searchKeyword: string;
private filterRoles: IFilterRole[];
/**
* Constructor method.

View File

@@ -1,12 +1,13 @@
import { FIELD_TYPE } from './constants';
import { DynamicFilterAbstractor } from './DynamicFilterAbstractor';
import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor';
interface ISortRole {
fieldKey: string;
order: string;
}
export class DynamicFilterSortBy extends DynamicFilterAbstractor {
export class DynamicFilterSortBy extends DynamicFilterRoleAbstractor {
private sortRole: ISortRole = {};
/**
@@ -36,7 +37,7 @@ export class DynamicFilterSortBy extends DynamicFilterAbstractor {
* @param field
* @returns {string}
*/
private getFieldComparatorRelationColumn = (field): string => {
protected getFieldComparatorRelationColumn(field: any): string {
const relation = this.model.relationMappings[field.relationKey];
if (relation) {
@@ -46,7 +47,7 @@ export class DynamicFilterSortBy extends DynamicFilterAbstractor {
return `${relationModel.tableName}.${relationField.column}`;
}
return '';
};
}
/**
* Retrieve the comparator field column.

View File

@@ -1,11 +1,9 @@
import { omit } from 'lodash';
import { IView, IViewRole } from '@/interfaces';
import { DynamicFilterAbstractor } from './DynamicFilterAbstractor';
import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor';
export class DynamicFilterViews extends DynamicFilterAbstractor {
export class DynamicFilterViews extends DynamicFilterRoleAbstractor {
private viewSlug: string;
private logicExpression: string;
private filterRoles: IViewRole[];
private viewColumns = [];
/**

View File

@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { DynamicListService } from './DynamicList.service';
import { DynamicListCustomView } from './DynamicListCustomView.service';
import { DynamicListSortBy } from './DynamicListSortBy.service';
import { DynamicListSearch } from './DynamicListSearch.service';
import { DynamicListFilterRoles } from './DynamicListFilterRoles.service';
@Module({
providers: [
DynamicListService,
DynamicListCustomView,
DynamicListSortBy,
DynamicListSearch,
DynamicListFilterRoles,
],
exports: [DynamicListService],
})
export class DynamicListModule {}

View File

@@ -1,18 +1,15 @@
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 { IDynamicListFilter } from './DynamicFilter/DynamicFilter.types';
import { DynamicListSortBy } from './DynamicListSortBy.service';
import { DynamicListSearch } from './DynamicListSearch.service';
import { DynamicListCustomView } from './DynamicListCustomView.service';
import { Injectable } from '@nestjs/common';
import { DynamicListFilterRoles } from './DynamicListFilterRoles';
import { DynamicListFilterRoles } from './DynamicListFilterRoles.service';
import { DynamicFilter } from './DynamicFilter';
import { BaseModel } from '@/models/Model';
@Injectable()
export class DynamicListService implements IDynamicListService {
export class DynamicListService {
constructor(
private dynamicListFilterRoles: DynamicListFilterRoles,
private dynamicListSearch: DynamicListSearch,
@@ -44,7 +41,10 @@ export class DynamicListService implements IDynamicListService {
* @param {IModel} model - Model.
* @param {IDynamicListFilter} filter - Dynamic filter DTO.
*/
public dynamicList = async (model: BaseModel, filter: IDynamicListFilter) => {
public dynamicList = async (
model: typeof BaseModel,
filter: IDynamicListFilter,
) => {
const dynamicFilter = new DynamicFilter(model);
// Parses the filter object.
@@ -90,7 +90,9 @@ export class DynamicListService implements IDynamicListService {
* Parses stringified filter roles.
* @param {string} stringifiedFilterRoles - Stringified filter roles.
*/
public parseStringifiedFilter = (filterRoles: IDynamicListFilter) => {
public parseStringifiedFilter<T extends IDynamicListFilter>(
filterRoles: T,
): T {
return {
...filterRoles,
filterRoles: filterRoles.stringifiedFilterRoles

View File

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

View File

@@ -1,12 +1,12 @@
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';
import { DynamicListServiceAbstract } from './DynamicListServiceAbstract';
@Injectable()
export class DynamicListCustomView extends DynamicListAbstract {
export class DynamicListCustomView extends DynamicListServiceAbstract {
/**
* Retreive custom view or throws error not found.
* @param {number} tenantId
@@ -30,7 +30,7 @@ export class DynamicListCustomView extends DynamicListAbstract {
* Dynamic list custom view.
* @param {IModel} model
* @param {number} customViewId
* @returns
* @returns {DynamicFilterRoleAbstractor}
*/
public dynamicListCustomView = async (
dynamicFilter: any,
@@ -40,6 +40,7 @@ export class DynamicListCustomView extends DynamicListAbstract {
// Retrieve the custom view or throw not found.
const view = await this.getCustomViewOrThrowError(customViewSlug, model);
return new DynamicFilterViews(view);
};
}

View File

@@ -2,14 +2,14 @@ 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';
import { DynamicFilterRoleAbstractor } from './DynamicFilter/DynamicFilterRoleAbstractor';
@Injectable()
export class DynamicListFilterRoles extends DynamicListAbstract {
export class DynamicListFilterRoles extends DynamicFilterRoleAbstractor {
/**
* Validates filter roles schema.
* @param {IFilterRole[]} filterRoles - Filter roles.

View File

@@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { DynamicListAbstract } from './DynamicListAbstract';
import { DynamicFilterSearch } from './DynamicFilter/DynamicFilterSearch';
import { DynamicListServiceAbstract } from './DynamicListServiceAbstract';
@Injectable()
export class DynamicListSearch extends DynamicListAbstract {
export class DynamicListSearch extends DynamicListServiceAbstract {
/**
* Dynamic list filter roles.
* @param {string} searchKeyword - Search keyword.

View File

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

View File

@@ -1,13 +1,13 @@
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';
import { DynamicFilterRoleAbstractor } from './DynamicFilter/DynamicFilterRoleAbstractor';
@Injectable()
export class DynamicListSortBy extends DynamicListAbstract {
export class DynamicListSortBy extends DynamicFilterRoleAbstractor {
/**
* Dynamic list sort by.
* @param {BaseModel} model

View File

@@ -0,0 +1,26 @@
import { BaseModel } from '@/models/Model';
export const CustomViewBaseModel = (Model: typeof BaseModel) =>
class extends Model {
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return [];
}
/**
* Retrieve the default view by the given slug.
*/
static getDefaultViewBySlug(viewSlug) {
return this.defaultViews.find((view) => view.slug === viewSlug) || null;
}
/**
* Retrieve the default views.
* @returns {IView[]}
*/
static getDefaultViews() {
return this.defaultViews;
}
};

View File

@@ -0,0 +1,93 @@
import { get } from 'lodash';
import {
IModelMeta,
IModelMetaField,
IModelMetaDefaultSort,
} from '@/interfaces/Model';
import { BaseModel } from '@/models/Model';
const defaultModelMeta = {
fields: {},
fields2: {},
};
export interface IMetadataModel extends BaseModel {
meta: IModelMeta;
parsedMeta: IModelMeta;
fields: { [key: string]: IModelMetaField };
defaultSort: IModelMetaDefaultSort;
defaultFilterField: string;
getField(key: string, attribute?: string): IModelMetaField;
getMeta(key?: string): IModelMeta;
}
type GConstructor<T = {}> = new (...args: any[]) => T;
export const MetadataModelMixin = <T extends GConstructor<BaseModel>>(
Model: T,
) =>
class ModelSettings extends Model {
/**
* Retrieve the model meta.
* @returns {IModelMeta}
*/
static get meta(): IModelMeta {
throw new Error('');
}
/**
* Parsed meta merged with default emta.
* @returns {IModelMeta}
*/
static get parsedMeta(): IModelMeta {
return {
...defaultModelMeta,
...this.meta,
};
}
/**
* Retrieve specific model field meta of the given field key.
* @param {string} key
* @returns {IModelMetaField}
*/
public static getField(key: string, attribute?: string): IModelMetaField {
const field = get(this.meta.fields, key);
return attribute ? get(field, attribute) : field;
}
/**
* Retrieves the specific model meta.
* @param {string} key
* @returns
*/
public static getMeta(key?: string) {
return key ? get(this.parsedMeta, key) : this.parsedMeta;
}
/**
* Retrieve the model meta fields.
* @return {{ [key: string]: IModelMetaField }}
*/
public static get fields(): { [key: string]: IModelMetaField } {
return this.getMeta('fields');
}
/**
* Retrieve the model default sort settings.
* @return {IModelMetaDefaultSort}
*/
public static get defaultSort(): IModelMetaDefaultSort {
return this.getMeta('defaultSort');
}
/**
* Retrieve the default filter field key.
* @return {string}
*/
public static get defaultFilterField(): string {
return this.getMeta('defaultFilterField');
}
};

View File

@@ -0,0 +1,24 @@
import { BaseModel } from '@/models/Model';
import { IModelMeta } from '@/interfaces/Model';
import { ISearchRole } from '../DynamicFilter.types';
type GConstructor<T = {}> = new (...args: any[]) => T;
export const SearchableBaseModelMixin = <T extends GConstructor<BaseModel>>(
Model: T,
) =>
class SearchableBaseModel extends Model {
/**
* Searchable model.
*/
static get searchable(): IModelMeta {
throw true;
}
/**
* Search roles.
*/
static get searchRoles(): ISearchRole[] {
return [];
}
};

View File

@@ -0,0 +1,11 @@
import { ISortOrder } from '@/interfaces/Model';
import { IFilterRole } from '../DynamicFilter/DynamicFilter.types';
export interface IDynamicListFilter {
customViewId?: number;
filterRoles?: IFilterRole[];
columnSortBy: ISortOrder;
sortOrder: string;
stringifiedFilterRoles: string;
searchKeyword?: string;
}