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, }; } } // ================================================================== }