pos-be/src/modules/reports/shared/helpers/query-builder.ts

528 lines
16 KiB
TypeScript

import { FILTER_TYPE } from '../constant';
import { QueryModelEntity } from '../entities/query-model.entity';
import {
ReportColumnConfigEntity,
ReportConfigEntity,
} from '../entities/report-config.entity';
export class ReportQueryBuilder {
public reportConfig: ReportConfigEntity;
public queryModel: QueryModelEntity;
constructor(reportConfig: ReportConfigEntity, queryModel: QueryModelEntity) {
this.reportConfig = reportConfig;
this.queryModel = queryModel;
}
getBaseConfig() {
const tableSchema = this.reportConfig.table_schema;
const mainTableAlias = this.reportConfig.main_table_alias ?? 'main';
const selectSql = this.createSelectSql();
const selectSqlExport = this.createSelectSqlExport();
const whereSql = this.createWhereSql();
const limitSql = this.createLimitSql();
const orderBySql = this.createOrderBySql();
const orderBySqlExport = this.createOrderBySqlExport();
const groupBy = this.createGroupBySql();
const groupByExport = this.createGroupBySqlExport();
return {
tableSchema,
mainTableAlias,
selectSql,
selectSqlExport,
whereSql,
limitSql,
orderBySql,
orderBySqlExport,
...groupBy,
...groupByExport,
};
}
getSql(): string {
const {
selectSql,
tableSchema,
whereSql,
groupByQuery,
orderBySql,
limitSql,
} = this.getBaseConfig();
return `SELECT ${selectSql} FROM ${tableSchema} ${whereSql} ${groupByQuery} ${orderBySql} ${limitSql}` as string;
}
getSqlCount(): string {
const { groupByColumn, mainTableAlias, tableSchema, whereSql } =
this.getBaseConfig();
return `SELECT COUNT(${
groupByColumn ? `DISTINCT ${groupByColumn}` : `${mainTableAlias}.id`
}) ${
groupByColumn
? `+ COUNT(DISTINCT CASE WHEN ${groupByColumn} IS NULL THEN 1 END)`
: ''
} AS count FROM ${tableSchema} ${whereSql}` as string;
}
getSqlExport(): string {
const {
selectSqlExport,
tableSchema,
whereSql,
groupByQueryExport,
orderBySqlExport,
} = this.getBaseConfig();
return `SELECT ${selectSqlExport} FROM ${tableSchema} ${whereSql} ${groupByQueryExport} ${orderBySqlExport}` as string;
}
getSqlCountExport(): string {
const { groupByColumnExport, mainTableAlias, tableSchema, whereSql } =
this.getBaseConfig();
return `SELECT COUNT(${
groupByColumnExport
? `DISTINCT ${groupByColumnExport}`
: `${mainTableAlias}.id`
}) ${
groupByColumnExport
? `+ COUNT(DISTINCT CASE WHEN ${groupByColumnExport} IS NULL THEN 1 END)`
: ''
} AS count FROM ${tableSchema} ${whereSql}` as string;
}
isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level. we are at the lowest level
// if we are grouping by more columns than we have keys for (that means the user
// has not expanded a lowest level group, OR we are not grouping at all).
return rowGroupCols.length > groupKeys.length;
}
interpolate(str, o) {
return str.replace(/{([^{}]*)}/g, function (a, b) {
const r = o[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
});
}
createLimitSql() {
const startRow = this.queryModel.startRow;
const endRow = this.queryModel.endRow;
const pageSize = endRow - startRow;
return ' LIMIT ' + (pageSize + 1) + ' OFFSET ' + startRow;
}
getRowCount(results) {
if (results === null || results === undefined || results.length === 0) {
return null;
}
const currentLastRow = this.queryModel.startRow + results.length;
return currentLastRow <= this.queryModel.endRow ? currentLastRow : -1;
}
cutResultsToPageSize(results) {
const pageSize = this.queryModel.endRow - this.queryModel.startRow;
if (results && results.length > pageSize) {
return results.splice(0, pageSize);
} else {
return results;
}
}
findQueryConfig(column: string) {
const configColumns = this.reportConfig.column_configs ?? [];
const queryColumn: ReportColumnConfigEntity = configColumns.find(
(el) => el.column === column,
);
const customQueryColumn = this.reportConfig?.customQueryColumn
? this.reportConfig.customQueryColumn(column)
: undefined;
if (customQueryColumn) return customQueryColumn;
else if (queryColumn) return queryColumn.query;
return column.replace('__', '.');
}
// GENERATE SELECT QUERY ============================================
createSelectSql(): string {
const configColumns = this.reportConfig.column_configs;
const mainTableAlias = this.reportConfig.main_table_alias ?? 'main';
const rowGroupCols = this.queryModel.rowGroupCols;
const valueCols = this.queryModel.valueCols;
const groupKeys = this.queryModel.groupKeys;
if (this.isDoingGrouping(rowGroupCols, groupKeys)) {
const colsToSelect = [];
const rowGroupCol = rowGroupCols[groupKeys.length];
const rowGroupColChildCount = rowGroupCols[groupKeys.length + 1];
colsToSelect.push(
this.findQueryConfig(rowGroupCol.field) + ` AS ${rowGroupCol.field}`,
);
if (rowGroupColChildCount) {
colsToSelect.push(
`COUNT(DISTINCT ${this.findQueryConfig(
rowGroupColChildCount.field,
)}) AS count_child_group`,
);
} else {
colsToSelect.push(`COUNT(${mainTableAlias}.id) AS count_child_group`);
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const thisSelf = this;
valueCols.forEach(function (valueCol) {
colsToSelect.push(
`${valueCol.aggFunc} (${thisSelf.findQueryConfig(
valueCol.field,
)}) AS ${valueCol.field}`,
);
});
return colsToSelect.join(', ');
}
const columns = configColumns.map(
(item) => `${item.query} AS ${item.column}`,
);
return columns.join(', ');
}
createSelectSqlExport(): string {
const configColumns = this.reportConfig.column_configs;
const rowGroupCols = this.queryModel.rowGroupCols;
const valueCols = this.queryModel.valueCols;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const thisSelf = this;
if (rowGroupCols.length > 0) {
const colsToSelect = [];
rowGroupCols.forEach(function (rowGroupCol) {
colsToSelect.push(
thisSelf.findQueryConfig(rowGroupCol.field) +
` AS ${rowGroupCol.field}`,
);
});
valueCols.forEach(function (valueCol) {
colsToSelect.push(
`${valueCol.aggFunc} (${thisSelf.findQueryConfig(
valueCol.field,
)}) AS ${valueCol.field}`,
);
});
return colsToSelect.join(', ');
}
const columns = configColumns.map((item) => `${item.query} ${item.column}`);
return columns.join(', ');
}
// ==================================================================
// GENERATE WHERE QUERY =============================================
createFilterSql(column: string, item: { type: FILTER_TYPE; filter: any }) {
switch (item.type) {
// TEXT
case FILTER_TYPE.TEXT_EQUAL:
return `${column} = '${item.filter}'`;
case FILTER_TYPE.TEXT_NOT_EQUAL:
return `${column} != '${item.filter}'`;
case FILTER_TYPE.TEXT_CONTAINS:
return `${column} ILIKE '%${item.filter}%'`;
case FILTER_TYPE.TEXT_NOT_CONTAINS:
return `${column} NOT ILIKE '%${item.filter}%'`;
case FILTER_TYPE.TEXT_START_WITH:
return `${column} ILIKE '${item.filter}%'`;
case FILTER_TYPE.TEXT_END_WITH:
return `${column} ILIKE '%${item.filter}'`;
case FILTER_TYPE.TEXT_IN_MEMBER:
return item.filter?.length > 0
? `${column} IN(${item.filter.map((i) => `'${i}'`).join(', ')})`
: null;
case FILTER_TYPE.TEXT_MULTIPLE_CONTAINS:
return item.filter?.length > 0
? `${column} ILIKE ANY(ARRAY[${item.filter
.map((i) => `'%${i}%'`)
.join(', ')}])`
: null;
case FILTER_TYPE.TEXT_MULTIPLE_REGEXP_CONTAINS:
return item.filter?.length > 0
? `${column} REGEXP '${item.filter.join('|')}'`
: null;
case FILTER_TYPE.DATE_IN_RANGE_EPOCH:
return `(${column} >= ${item.filter[0]} AND ${column} <= ${item.filter[1]})`;
case FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP:
return `(${column} BETWEEN '${item.filter[0]}' AND '${item.filter[1]}')`;
case FILTER_TYPE.TEXT_IN_RANGE:
return `(${column} >= '${item.filter[0]}' AND ${column} <= '${item.filter[1]}')`;
// NUMBER
case FILTER_TYPE.NUMBER_EQUAL:
return `${column} = ${item.filter}`;
case FILTER_TYPE.NUMBER_NOT_EQUAL:
return `${column} != ${item.filter}`;
case FILTER_TYPE.NUMBER_GREATER_THAN:
return `${column} > ${item.filter}`;
case FILTER_TYPE.NUMBER_GREATER_THAN_OR_EQUAL:
return `${column} >= ${item.filter}`;
case FILTER_TYPE.NUMBER_LESS_THAN:
return `${column} < ${item.filter}`;
case FILTER_TYPE.NUMBER_LESS_THAN_OR_EQUAL:
return `${column} <= ${item.filter}`;
case FILTER_TYPE.NUMBER_IN_RANGE:
return `(${column} >= ${item.filter[0]} AND ${column} <= ${item.filter[1]})`;
default:
console.log('UNKNOWN FILTER TYPE:', item.type);
return 'true';
}
}
createWhereSql() {
const configFilters = this.reportConfig.filter_configs ?? [];
const ignoreFilterKeys = this.reportConfig.ignore_filter_keys ?? [];
const ignoreFilter = configFilters
.filter((el) => el.hide_field)
.map((el) => el.filter_column);
const ignoreFilterKey = [...ignoreFilter, ...ignoreFilterKeys];
const whereCondition = this.reportConfig?.whereCondition
? this.reportConfig.whereCondition(this.queryModel.filterModel)
: [];
const rowGroupCols = this.queryModel.rowGroupCols;
const groupKeys = this.queryModel.groupKeys;
const filterModel = this.queryModel.filterModel;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const thisSelf = this;
const whereParts = [];
if (groupKeys.length > 0) {
groupKeys.forEach(function (key, index) {
const colName = rowGroupCols[index].field;
// whereParts.push(colName + ' = "' + key + '"');
if (!key) {
whereParts.push(`${thisSelf.findQueryConfig(colName)} is null`);
} else {
whereParts.push(`${thisSelf.findQueryConfig(colName)} = '${key}'`);
}
});
}
if (filterModel) {
const keySet = Object.keys(filterModel);
keySet.forEach(function (key) {
if (!ignoreFilterKey.includes(key)) {
const item = filterModel[key];
const newKey = thisSelf.findQueryConfig(key);
whereParts.push(thisSelf.createFilterSql(newKey, item));
}
});
}
// set default where conditions
const defaultConditions = this.reportConfig.whereDefaultConditions;
const defaultWhereOptions = [];
if (defaultConditions) {
defaultConditions.forEach((condition) => {
defaultWhereOptions.push(
this.createFilterSql(condition.column, {
type: condition.filter_type,
filter: condition.values,
}),
);
});
}
const tableWhereConditions = [...whereCondition, ...whereParts].filter(
Boolean,
);
const defaultWhereConditions = defaultWhereOptions.filter(Boolean);
if (tableWhereConditions.length > 0) {
return `WHERE (${
defaultWhereConditions.length
? defaultWhereConditions?.filter(Boolean).join(' AND ') + ' AND '
: ' '
} ${tableWhereConditions.filter(Boolean).join(' AND ')})`;
} else if (defaultWhereConditions.length) {
return `WHERE (${defaultWhereConditions.filter(Boolean).join(' AND ')})`;
} else {
return '';
}
}
// ==================================================================
// GENERATE ORDER QUERY =============================================
createOrderBySql() {
const mainTableAlias = this.reportConfig.main_table_alias ?? 'main';
const defaultOrderBy = this.reportConfig.defaultOrderBy ?? [];
const lowLevelOrderBy = this.reportConfig.lowLevelOrderBy ?? [];
const rowGroupCols = this.queryModel.rowGroupCols;
const groupKeys = this.queryModel.groupKeys;
const sortModel = this.queryModel.sortModel;
const grouping = this.isDoingGrouping(rowGroupCols, groupKeys);
const sortParts = [];
if (sortModel) {
const groupColIds = rowGroupCols
.map((groupCol) => groupCol.id)
.slice(0, groupKeys.length + 1);
sortModel.forEach(function (item) {
if (grouping && groupColIds.indexOf(item.colId) < 0) {
// ignore
} else {
sortParts.push(item.colId + ' ' + item.sort);
}
});
}
const defaultOrder = defaultOrderBy[0]
? defaultOrderBy
: [`${mainTableAlias}.created_at DESC`];
const lowLevelOrder = lowLevelOrderBy[0]
? lowLevelOrderBy
: [`${mainTableAlias}.id DESC`];
if (sortParts.length > 0) {
if (rowGroupCols?.length > 0) {
if (groupKeys.length > 0) {
const sortBy = sortParts[groupKeys.length];
if (sortBy) return ' ORDER BY ' + sortBy;
// return '';
}
return ' ORDER BY ' + sortParts.join(', ');
}
return (
' ORDER BY ' + sortParts.join(', ') + `, ${lowLevelOrder.join(', ')}`
);
} else {
if (rowGroupCols?.length > 0) {
const sortGroupData = rowGroupCols[groupKeys.length];
if (sortGroupData) return ' ORDER BY ' + `${sortGroupData['id']} DESC`;
// return '';
}
return (
` ORDER BY ` + defaultOrder.join(', ') + `, ${lowLevelOrder.join(', ')}`
);
}
}
createOrderBySqlExport() {
const mainTableAlias = this.reportConfig.main_table_alias ?? 'main';
const defaultOrderBy = this.reportConfig.defaultOrderBy ?? [];
const lowLevelOrderBy = this.reportConfig.lowLevelOrderBy ?? [];
const rowGroupCols = this.queryModel.rowGroupCols;
const sortModel = this.queryModel.sortModel;
const defaultOrder = defaultOrderBy[0]
? defaultOrderBy
: [`${mainTableAlias}.created_at DESC`];
const lowLevelOrder = lowLevelOrderBy[0]
? lowLevelOrderBy
: [`${mainTableAlias}.id DESC`];
if (sortModel.length > 0) {
const sortParts = sortModel.map((i) => `${i.colId} ${i.sort}`);
return ' ORDER BY ' + `${sortParts.join(', ')}`;
} else {
if (rowGroupCols?.length > 0) {
const sortParts = rowGroupCols.map((i) => `${i.id} DESC`);
return ' ORDER BY ' + `${sortParts.join(', ')}`;
}
return (
` ORDER BY ` + defaultOrder.join(', ') + `, ${lowLevelOrder.join(', ')}`
);
}
}
// ==================================================================
// GENERATE GROUP BY QUERY ==========================================
createGroupBySql() {
const rowGroupCols = this.queryModel.rowGroupCols;
const groupKeys = this.queryModel.groupKeys;
if (this.isDoingGrouping(rowGroupCols, groupKeys)) {
const colsToGroupBy = [];
const rowGroupCol = rowGroupCols[groupKeys.length];
colsToGroupBy.push(this.findQueryConfig(rowGroupCol.field));
return {
groupByQuery: ' GROUP BY ' + colsToGroupBy.join(', '),
groupByColumn: colsToGroupBy.join(', '),
};
} else {
return {
groupByQuery: '',
groupByColumn: null,
};
}
}
createGroupBySqlExport() {
const rowGroupCols = this.queryModel.rowGroupCols;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const thisSelf = this;
if (rowGroupCols.length > 0) {
const colsToGroupBy = [];
rowGroupCols.forEach(function (rowGroupCol) {
colsToGroupBy.push(thisSelf.findQueryConfig(rowGroupCol.field));
});
return {
groupByQueryExport: ' GROUP BY ' + colsToGroupBy.join(', '),
groupByColumnExport: colsToGroupBy.join(', '),
};
} else {
return {
groupByQueryExport: '',
groupByColumnExport: null,
};
}
}
// ==================================================================
}