mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 {}
|
||||
@@ -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))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export class DynamicListServiceAbstract {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
packages/server/src/modules/DynamicListing/constants.ts
Normal file
6
packages/server/src/modules/DynamicListing/constants.ts
Normal 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',
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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 [];
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user