feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,90 @@
import { forEach } from 'lodash';
import { DynamicFilterAbstractor } from './DynamicFilterAbstractor';
import { IFilterRole } from './DynamicFilter.types';
import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor';
import { MetableModel } from '../types/DynamicList.types';
export class DynamicFilter<R extends {}> extends DynamicFilterAbstractor {
public model: MetableModel;
public dynamicFilters: DynamicFilterRoleAbstractor[];
/**
* Constructor.
* @param {MetableModel} model - Metable model.
*/
constructor(model: MetableModel) {
super();
this.model = model;
this.dynamicFilters = [];
}
/**
* Registers the given dynamic filter.
* @param {IDynamicFilter} filterRole - Filter role.
*/
public setFilter = (dynamicFilter: DynamicFilterRoleAbstractor) => {
dynamicFilter.setModel(this.model);
dynamicFilter.onInitialize();
this.dynamicFilters.push(dynamicFilter);
};
/**
* Retrieve dynamic filter build queries.
* @returns {Function[]}
*/
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 = (): R => {
const responseMeta = {};
this.dynamicFilters.forEach((filter) => {
const filterMeta = filter.getResponseMeta();
forEach(filterMeta, (value, key) => {
responseMeta[key] = value;
});
});
return responseMeta as R;
};
}

View File

@@ -0,0 +1,40 @@
import { BaseModel } from '@/models/Model';
export type ISortOrder = 'DESC' | 'ASC';
export interface IDynamicFilter {
setModel(model: typeof BaseModel): void;
onInitialize(): 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: ISortOrder;
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,56 @@
// @ts-nocheck
import { IDynamicFilter } from './DynamicFilter.types';
import { MetableModel } from '../types/DynamicList.types';
export class DynamicFilterAbstractor {
public model: MetableModel;
public dynamicFilters: IDynamicFilter[];
/**
* Extract relation table name from relation.
* @param {String} column - Column name
* @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,25 @@
import { IFilterRole } from './DynamicFilter.types';
import { DynamicFilterFilterRoles } from './DynamicFilterFilterRoles';
export class DynamicFilterAdvancedFilter extends DynamicFilterFilterRoles {
/**
* 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,49 @@
import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor';
export class DynamicFilterFilterRoles extends DynamicFilterRoleAbstractor {
/**
* 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.
*/
public 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,73 @@
// @ts-nocheck
import { OPERATION } from '@/libs/logic-evaluation/Parser';
export class DynamicFilterQueryParser {
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,400 @@
// @ts-nocheck
import * as 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';
import { MetableModel } from '../types/DynamicList.types';
import { Knex } from 'knex';
export abstract class DynamicFilterRoleAbstractor implements IDynamicFilter {
public filterRoles: IFilterRole[] = [];
public tableName: string;
public model: MetableModel;
public responseMeta: { [key: string]: any } = {};
public relationFields = [];
/**
* Sets model the dynamic filter service.
* @param {IModel} model
*/
public setModel(model: MetableModel) {
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: typeof BaseModel,
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: typeof BaseModel,
roles: IFilterRole[],
logicExpression: string,
) => {
const basicExpression = this.parseLogicExpression(logicExpression);
return (builder) => {
this.buildFilterRolesQuery(model, roles, basicExpression)(builder);
};
};
/**
* Retrieve relation column of comparator fieldز
*/
protected getFieldComparatorRelationColumn(field: any): string {
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 {} -
*/
protected 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: MetableModel, 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() {}
/**
* Builds the query.
*/
buildQuery(): (builder: Knex.QueryBuilder) => void {
throw new Error('Method not implemented.');
}
/**
* Retrieves the response meta.
*/
getResponseMeta() {
throw new Error('Method not implemented.');
}
}

View File

@@ -0,0 +1,61 @@
import { IFilterRole } from './DynamicFilter.types';
import { DynamicFilterFilterRoles } from './DynamicFilterFilterRoles';
export interface IDynamicFilterSearchResponseMeta {
searchKeyword: string;
}
export class DynamicFilterSearch extends DynamicFilterFilterRoles {
private searchKeyword: string;
/**
* 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,
}));
}
/**
* Sets the response meta.
*/
setResponseMeta() {
this.responseMeta = {
searchKeyword: this.searchKeyword,
};
}
/**
* Retrieves the response meta.
* @returns {IDynamicFilterSearchResponseMeta}
*/
public getResponseMeta(): IDynamicFilterSearchResponseMeta {
return {
searchKeyword: this.searchKeyword,
};
}
}

View File

@@ -0,0 +1,94 @@
import { FIELD_TYPE } from './constants';
import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor';
interface ISortRole {
fieldKey: string;
order: string;
}
export class DynamicFilterSortBy extends DynamicFilterRoleAbstractor {
private sortRole: ISortRole = {
fieldKey: '',
order: '',
};
/**
* Constructor method.
* @param {string} sortByFieldKey
* @param {string} sortDirection
*/
constructor(sortByFieldKey: string, sortDirection: string) {
super();
this.sortRole = {
fieldKey: sortByFieldKey,
order: sortDirection,
};
}
/**
* On initialize the dyanmic sort by.
*/
public onInitialize() {
this.setRelationIfRelationField(this.sortRole.fieldKey);
}
/**
* Retrieve field comparator relatin column.
* @param field
* @returns {string}
*/
protected getFieldComparatorRelationColumn(field: any): 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}
*/
getFieldComparatorColumn = (field) => {
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 getResponseMeta(): ISortRole {
return {
fieldKey: this.sortRole.fieldKey,
order: this.sortRole.order,
};
}
}

View File

@@ -0,0 +1,55 @@
import { omit } from 'lodash';
import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor';
import { IView } from '@/modules/Views/Views.types';
export class DynamicFilterViews extends DynamicFilterRoleAbstractor {
private viewSlug: string;
private logicExpression: string;
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,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

@@ -0,0 +1,106 @@
import { castArray, isEmpty } from 'lodash';
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.service';
import { DynamicFilter } from './DynamicFilter';
import { MetableModel } from './types/DynamicList.types';
import { IFilterMeta } from '@/interfaces/Model';
@Injectable()
export class DynamicListService {
constructor(
private dynamicListFilterRoles: DynamicListFilterRoles,
private dynamicListSearch: DynamicListSearch,
private dynamicListSortBy: DynamicListSortBy,
private dynamicListView: DynamicListCustomView,
) {}
/**
* Parses filter DTO.
* @param {MetableModel} model - Metable model.
* @param {IDynamicListFilter} filterDTO - Dynamic list filter DTO.
*/
private parseFilterObject = (
model: MetableModel,
filterDTO: IDynamicListFilter,
) => {
return {
// Merges the default properties with filter object.
...(model.defaultSort
? {
sortOrder: model.defaultSort.sortOrder,
columnSortBy: model.defaultSort.sortOrder,
}
: {}),
...filterDTO,
};
};
/**
* Dynamic listing.
* @param {IModel} model - Metable model.
* @param {IDynamicListFilter} filter - Dynamic filter DTO.
*/
public dynamicList = async (
model: MetableModel,
filter: IDynamicListFilter,
) => {
const dynamicFilter = new DynamicFilter<IFilterMeta>(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<T extends IDynamicListFilter>(
filterRoles: T,
): T {
return {
...filterRoles,
filterRoles: filterRoles.stringifiedFilterRoles
? castArray(JSON.parse(filterRoles.stringifiedFilterRoles))
: [],
};
}
}

View File

@@ -0,0 +1,47 @@
import { Injectable } from '@nestjs/common';
import { ERRORS } from './constants';
import { DynamicFilterViews } from './DynamicFilter';
import { ServiceError } from '../Items/ServiceError';
import { DynamicListServiceAbstract } from './DynamicListServiceAbstract';
import { IView } from '../Views/Views.types';
import { MetableModel } from './types/DynamicList.types';
@Injectable()
export class DynamicListCustomView extends DynamicListServiceAbstract {
/**
* Retreive custom view or throws error not found.
* @param {string} viewSlug - View slug.
* @param {MetableModel} model - Metable model.
* @return {Promise<IView>}
*/
private async getCustomViewOrThrowError(
viewSlug: string,
model: MetableModel,
): Promise<IView> {
// 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 {DynamicFilter} dynamicFilter - Dynamic filter.
* @param {string} customViewSlug - Custom view slug.
* @returns {DynamicFilterRoleAbstractor}
*/
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 { DynamicFilterAdvancedFilter } from './DynamicFilter/DynamicFilterAdvancedFilter';
import { DynamicFilterRoleAbstractor } from './DynamicFilter/DynamicFilterRoleAbstractor';
import { MetableModel } from './types/DynamicList.types';
import { ServiceError } from '../Items/ServiceError';
import { ERRORS } from './constants';
@Injectable()
export class DynamicListFilterRoles extends DynamicFilterRoleAbstractor {
/**
* 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 {MetableModel} model
* @param {IFilterRole} filterRoles
* @returns {string[]}
*/
private getFilterRolesFieldsNotExist = (
model: MetableModel,
filterRoles: IFilterRole[],
): string[] => {
return filterRoles
.filter((filterRole) => !model.getField(filterRole.fieldKey))
.map((filterRole) => filterRole.fieldKey);
};
/**
* Validates existance the fields of filter roles.
* @param {MetableModel} model
* @param {IFilterRole[]} filterRoles
* @throws {ServiceError}
*/
private validateFilterRolesFieldsExistance = (
model: MetableModel,
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 {MetableModel} model - Metable model.
* @param {IFilterRole[]} filterRoles - Filter roles.
* @returns {DynamicFilterFilterRoles}
*/
public dynamicList = (
model: MetableModel,
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 { DynamicFilterSearch } from './DynamicFilter/DynamicFilterSearch';
import { DynamicListServiceAbstract } from './DynamicListServiceAbstract';
@Injectable()
export class DynamicListSearch extends DynamicListServiceAbstract {
/**
* Dynamic list filter roles.
* @param {string} searchKeyword - Search keyword.
* @returns {DynamicFilterFilterRoles}
*/
public dynamicSearch = (searchKeyword: string) => {
return new DynamicFilterSearch(searchKeyword);
};
}

View File

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

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
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 { DynamicFilterAbstractor } from './DynamicFilter/DynamicFilterAbstractor';
import { MetableModel } from './types/DynamicList.types';
@Injectable()
export class DynamicListSortBy extends DynamicFilterAbstractor {
/**
* Dynamic list sort by.
* @param {BaseModel} model
* @param {string} columnSortBy
* @param {ISortOrder} sortOrder
* @returns {DynamicFilterSortBy}
*/
public dynamicSortBy(
model: MetableModel,
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',
};

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 {
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,28 @@
import { BaseModel } from '@/models/Model';
import { IModelMeta } from '@/interfaces/Model';
import { ISearchRole } from '../DynamicFilter/DynamicFilter.types';
type GConstructor<T = {}> = new (...args: any[]) => T;
export interface ISearchableBaseModel {
searchRoles: ISearchRole[];
}
export const SearchableBaseModelMixin = <T extends GConstructor<BaseModel>>(
Model: T,
) =>
class SearchableBaseModel extends Model {
/**
* Searchable model.
*/
static get searchable(): boolean {
throw true;
}
/**
* Search roles.
*/
static get searchRoles(): ISearchRole[] {
return [];
}
};

View File

@@ -0,0 +1,20 @@
import { ISortOrder } from '@/interfaces/Model';
import { BaseModel } from '@/models/Model';
import { ICustomViewBaseModel } from '@/modules/CustomViews/CustomViewBaseModel';
import { IFilterRole } from '../DynamicFilter/DynamicFilter.types';
import { IMetadataModel } from '../models/MetadataModel';
import { ISearchableBaseModel } from '../models/SearchableBaseModel';
export interface IDynamicListFilter {
customViewId?: number;
filterRoles?: IFilterRole[];
columnSortBy: ISortOrder;
sortOrder: string;
stringifiedFilterRoles: string;
searchKeyword?: string;
}
export type MetableModel = typeof BaseModel &
IMetadataModel &
ISearchableBaseModel &
ICustomViewBaseModel;