From c3ffb2b13f2310a3aef2a0b9c54ed61f45c90041 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:47:20 +0700 Subject: [PATCH 1/4] feat: init query builder report --- .../reports/report/report.controller.ts | 5 + src/modules/reports/report/report.service.ts | 6 +- .../shared/configs/general-report/index.ts | 1 + src/modules/reports/shared/configs/index.ts | 4 + .../shared/configs/tenant-report/index.ts | 1 + .../shared/entities/report-config.entity.ts | 59 ++ src/modules/reports/shared/helpers/index.ts | 1 + .../reports/shared/helpers/query-builder.ts | 506 ++++++++++++++++++ 8 files changed, 582 insertions(+), 1 deletion(-) create mode 100644 src/modules/reports/shared/configs/general-report/index.ts create mode 100644 src/modules/reports/shared/configs/tenant-report/index.ts create mode 100644 src/modules/reports/shared/entities/report-config.entity.ts create mode 100644 src/modules/reports/shared/helpers/index.ts create mode 100644 src/modules/reports/shared/helpers/query-builder.ts diff --git a/src/modules/reports/report/report.controller.ts b/src/modules/reports/report/report.controller.ts index 5ccbb55..315474b 100644 --- a/src/modules/reports/report/report.controller.ts +++ b/src/modules/reports/report/report.controller.ts @@ -21,4 +21,9 @@ export class ReportController { async getReportData(@Body() body: GetReportDataDto) { return await this.service.getReportData(body); } + + @Post('meta') + async getReportMeta(@Body() body: GetReportDataDto) { + return await this.service.getReportMeta(body); + } } diff --git a/src/modules/reports/report/report.service.ts b/src/modules/reports/report/report.service.ts index 390737d..0474f56 100644 --- a/src/modules/reports/report/report.service.ts +++ b/src/modules/reports/report/report.service.ts @@ -2,14 +2,18 @@ import { Injectable } from '@nestjs/common'; import { BaseReportService } from '../shared/services/base-report.service'; import { GetReportConfigDto } from '../shared/dto/report-config.get.dto'; import { GetReportDataDto } from '../shared/dto/report-data.get.dto'; +import { ReportConfigs } from '../shared/configs'; @Injectable() export class ReportService extends BaseReportService { async getReportConfig(query: GetReportConfigDto) { - return 'you hit API for get report config'; + return ReportConfigs; } async getReportData(body: GetReportDataDto) { return 'you hit API for get report data'; } + async getReportMeta(body: GetReportDataDto) { + return 'you hit API for get report meta'; + } } diff --git a/src/modules/reports/shared/configs/general-report/index.ts b/src/modules/reports/shared/configs/general-report/index.ts new file mode 100644 index 0000000..9d6a412 --- /dev/null +++ b/src/modules/reports/shared/configs/general-report/index.ts @@ -0,0 +1 @@ +export const GeneralReportConfig = []; diff --git a/src/modules/reports/shared/configs/index.ts b/src/modules/reports/shared/configs/index.ts index e69de29..1ec9aab 100644 --- a/src/modules/reports/shared/configs/index.ts +++ b/src/modules/reports/shared/configs/index.ts @@ -0,0 +1,4 @@ +import { GeneralReportConfig } from './general-report'; +import { TenantReportConfig } from './tenant-report'; + +export const ReportConfigs = [...GeneralReportConfig, ...TenantReportConfig]; diff --git a/src/modules/reports/shared/configs/tenant-report/index.ts b/src/modules/reports/shared/configs/tenant-report/index.ts new file mode 100644 index 0000000..3893e35 --- /dev/null +++ b/src/modules/reports/shared/configs/tenant-report/index.ts @@ -0,0 +1 @@ +export const TenantReportConfig = []; diff --git a/src/modules/reports/shared/entities/report-config.entity.ts b/src/modules/reports/shared/entities/report-config.entity.ts new file mode 100644 index 0000000..90dc77f --- /dev/null +++ b/src/modules/reports/shared/entities/report-config.entity.ts @@ -0,0 +1,59 @@ +import { DATA_FORMAT, DATA_TYPE, FILTER_TYPE } from '../constant'; + +export interface ReportColumnConfigEntity { + column: string; + query: string; + label: string; + type: DATA_TYPE; + format: DATA_FORMAT; + date_format?: string; +} + +export interface FilterConfigEntity { + filter_column: string; + filter_type: FILTER_TYPE; + + filed_label: string; + field_type: string; + hide_field?: boolean; + + select_data_source_url?: string; + select_custom_options?: string[]; + select_value_key?: string; + select_label_key?: string; +} + +export interface FilterPeriodConfigEntity { + key: string; + type: FILTER_TYPE; + note?: string; + hidden?: boolean; +} + +export interface ReportConfigEntity { + group_name: string; + unique_name: string; + label: string; + + table_schema: string; + main_table_alias?: string; + customVirtualTableSchema?( + filterModel: any, + findQueryConfig: (column: string) => string, + createFilterSql: (key: string, item: any) => string, + ): string; + whereCondition?(filterModel: any): string[]; + whereDefaultConditions?: { + column: string; + filter_type: FILTER_TYPE; + values: string[]; + }[]; + defaultOrderBy?: string[]; + lowLevelOrderBy?: string[]; + + column_configs: ReportColumnConfigEntity[]; + filter_configs?: FilterConfigEntity[]; + filter_period_config?: FilterPeriodConfigEntity; + ignore_filter_keys?: string[]; + customQueryFilter?(column: string): string; +} diff --git a/src/modules/reports/shared/helpers/index.ts b/src/modules/reports/shared/helpers/index.ts new file mode 100644 index 0000000..0fe5354 --- /dev/null +++ b/src/modules/reports/shared/helpers/index.ts @@ -0,0 +1 @@ +export * from './query-builder'; diff --git a/src/modules/reports/shared/helpers/query-builder.ts b/src/modules/reports/shared/helpers/query-builder.ts new file mode 100644 index 0000000..458bfe1 --- /dev/null +++ b/src/modules/reports/shared/helpers/query-builder.ts @@ -0,0 +1,506 @@ +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 queryModel = this.queryModel; + return { + tableSchema, + mainTableAlias, + queryModel, + }; + } + + // OLD VERSION + buildSql(reportConfig: ReportConfigEntity, queryModel: any) { + const tableSchema = reportConfig.table_schema; + const mainTableAlias = reportConfig.main_table_alias ?? 'main'; + + const selectSql = this.createSelectSql(queryModel, reportConfig); + const selectSqlExport = this.createSelectSqlExport( + queryModel, + reportConfig, + ); + + const whereSql = this.createWhereSql(queryModel, reportConfig); + const limitSql = this.createLimitSql(queryModel); + + const orderBySql = this.createOrderBySql(queryModel, reportConfig); + const orderBySqlExport = this.createOrderBySqlExport( + queryModel, + reportConfig, + ); + + const { groupByQuery, groupByColumn } = this.createGroupBySql( + queryModel, + reportConfig, + ); + const { groupByQueryExport, groupByColumnExport } = + this.createGroupBySqlExport(queryModel, reportConfig); + + const SQL: string = + `SELECT ${selectSql} FROM ${tableSchema} ${whereSql} ${groupByQuery} ${orderBySql} ${limitSql}` as string; + const SQL_COUNT: string = `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; + const SQL_EXPORT: string = + `SELECT ${selectSqlExport} FROM ${tableSchema} ${whereSql} ${groupByQueryExport} ${orderBySqlExport}` as string; + const SQL_EXPORT_COUNT: string = `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; + + return { SQL, SQL_COUNT, SQL_EXPORT, SQL_EXPORT_COUNT }; + } + + createSelectSql(queryModel: any, reportConfig: ReportConfigEntity): string { + const configColumns = reportConfig.column_configs; + const mainTableAlias = reportConfig.main_table_alias ?? 'main'; + + const rowGroupCols = queryModel.rowGroupCols; + const valueCols = queryModel.valueCols; + const groupKeys = queryModel.groupKeys; + + if (this.isDoingGrouping(rowGroupCols, groupKeys)) { + const colsToSelect = []; + const rowGroupCol = rowGroupCols[groupKeys.length]; + const rowGroupColChildCount = rowGroupCols[groupKeys.length + 1]; + + colsToSelect.push( + this.findQueryConfig(reportConfig, rowGroupCol.field) + + ` AS ${rowGroupCol.field}`, + ); + + if (rowGroupColChildCount) { + colsToSelect.push( + `COUNT(DISTINCT ${this.findQueryConfig( + reportConfig, + 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( + reportConfig, + valueCol.field, + )}) AS ${valueCol.field}`, + ); + }); + return colsToSelect.join(', '); + } + + const columns = configColumns.map( + (item) => `${item.query} AS ${item.column}`, + ); + + return columns.join(', '); + } + + createSelectSqlExport( + queryModel: any, + reportConfig: ReportConfigEntity, + ): string { + const configColumns = reportConfig.column_configs; + + const rowGroupCols = queryModel.rowGroupCols; + const valueCols = 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(reportConfig, rowGroupCol.field) + + ` AS ${rowGroupCol.field}`, + ); + }); + + valueCols.forEach(function (valueCol) { + colsToSelect.push( + `${valueCol.aggFunc} (${thisSelf.findQueryConfig( + reportConfig, + valueCol.field, + )}) AS ${valueCol.field}`, + ); + }); + + return colsToSelect.join(', '); + } + + const columns = configColumns.map((item) => `${item.query} ${item.column}`); + + return columns.join(', '); + } + + 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(queryModel, reportConfig: ReportConfigEntity) { + // const configColumns = reportConfig.column_configs ?? []; + const configFilters = reportConfig.filter_configs ?? []; + const ignoreFilterKeys = reportConfig.ignore_filter_keys ?? []; + const ignoreFilter = configFilters + .filter((el) => el.hide_field) + .map((el) => el.filter_column); + + const ignoreFilterKey = [...ignoreFilter, ...ignoreFilterKeys]; + + const whereCondition = reportConfig?.whereCondition + ? reportConfig.whereCondition(queryModel.filterModel) + : []; + + const rowGroupCols = queryModel.rowGroupCols; + const groupKeys = queryModel.groupKeys; + const filterModel = 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 + '"'); + whereParts.push( + `${thisSelf.findQueryConfig(reportConfig, 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(reportConfig, key); + whereParts.push(thisSelf.createFilterSql(newKey, item)); + } + }); + } + + // set default where conditions + const defaultConditions = 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]; + const defaultWhereConditions = defaultWhereOptions; + + 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 ''; + } + } + + createOrderBySql(queryModel, reportConfig: ReportConfigEntity) { + const mainTableAlias = reportConfig.main_table_alias ?? 'main'; + const defaultOrderBy = reportConfig.defaultOrderBy ?? []; + const lowLevelOrderBy = reportConfig.lowLevelOrderBy ?? []; + + const rowGroupCols = queryModel.rowGroupCols; + const groupKeys = queryModel.groupKeys; + const sortModel = 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(queryModel, reportConfig: ReportConfigEntity) { + const mainTableAlias = reportConfig.main_table_alias ?? 'main'; + const defaultOrderBy = reportConfig.defaultOrderBy ?? []; + const lowLevelOrderBy = reportConfig.lowLevelOrderBy ?? []; + + const rowGroupCols = queryModel.rowGroupCols; + const sortModel = 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(', ')}` + ); + } + } + + createGroupBySql(queryModel, reportConfig: ReportConfigEntity) { + // const configColumns = reportConfig.column_configs; + const rowGroupCols = queryModel.rowGroupCols; + const groupKeys = queryModel.groupKeys; + + if (this.isDoingGrouping(rowGroupCols, groupKeys)) { + const colsToGroupBy = []; + + const rowGroupCol = rowGroupCols[groupKeys.length]; + colsToGroupBy.push(this.findQueryConfig(reportConfig, rowGroupCol.field)); + + return { + groupByQuery: ' GROUP BY ' + colsToGroupBy.join(', '), + groupByColumn: colsToGroupBy.join(', '), + }; + } else { + return { + groupByQuery: '', + groupByColumn: null, + }; + } + } + + createGroupBySqlExport(queryModel, reportConfig: ReportConfigEntity) { + // const configColumns = reportConfig.column_configs; + const rowGroupCols = 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(reportConfig, rowGroupCol.field), + ); + }); + + return { + groupByQueryExport: ' GROUP BY ' + colsToGroupBy.join(', '), + groupByColumnExport: colsToGroupBy.join(', '), + }; + } else { + return { + groupByQueryExport: '', + groupByColumnExport: null, + }; + } + } + + findQueryConfig(config: ReportConfigEntity, column: string) { + const configColumns = config.column_configs ?? []; + + const findQuery: ReportColumnConfigEntity = configColumns.find( + (el) => el.column === column, + ); + const customQuery = config?.customQueryFilter + ? config.customQueryFilter(column) + : undefined; + + if (customQuery) return customQuery; + else if (findQuery) return findQuery.query; + + return column.replace('__', '.'); + } + + 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(queryModel) { + const startRow = queryModel.startRow; + const endRow = queryModel.endRow; + const pageSize = endRow - startRow; + return ' LIMIT ' + (pageSize + 1) + ' OFFSET ' + startRow; + } + + getRowCount(queryModel, results) { + if (results === null || results === undefined || results.length === 0) { + return null; + } + const currentLastRow = queryModel.startRow + results.length; + return currentLastRow <= queryModel.endRow ? currentLastRow : -1; + } + + cutResultsToPageSize(queryModel, results) { + const pageSize = queryModel.endRow - queryModel.startRow; + if (results && results.length > pageSize) { + return results.splice(0, pageSize); + } else { + return results; + } + } +} -- 2.40.1 From 4dc9f7ee9994e8c23bda064314cbf2f95d89551c Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:04:40 +0700 Subject: [PATCH 2/4] feat: setup query builder report and create sample report configuration --- .../general-report/configs/sample.report.ts | 40 +++ .../shared/configs/general-report/index.ts | 6 +- src/modules/reports/shared/configs/index.ts | 6 +- .../tenant-report/configs/sample.report.ts | 39 +++ .../shared/configs/tenant-report/index.ts | 5 +- .../shared/constant/report-group.constant.ts | 3 +- .../shared/entities/report-config.entity.ts | 2 +- .../reports/shared/helpers/query-builder.ts | 304 +++++++++--------- 8 files changed, 255 insertions(+), 150 deletions(-) create mode 100644 src/modules/reports/shared/configs/general-report/configs/sample.report.ts create mode 100644 src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts diff --git a/src/modules/reports/shared/configs/general-report/configs/sample.report.ts b/src/modules/reports/shared/configs/general-report/configs/sample.report.ts new file mode 100644 index 0000000..cabbd03 --- /dev/null +++ b/src/modules/reports/shared/configs/general-report/configs/sample.report.ts @@ -0,0 +1,40 @@ +import { DATA_FORMAT, DATA_TYPE, REPORT_GROUP } from '../../../constant'; +import { ReportConfigEntity } from '../../../entities/report-config.entity'; + +export default { + group_name: REPORT_GROUP.general_report, + unique_name: `${REPORT_GROUP.general_report}__sample`, + label: 'Sample General Report ', + table_schema: 'season_types', + main_table_alias: 'main', + defaultOrderBy: [], + lowLevelOrderBy: [], + filter_period_config: { + hidden: true, + }, + + column_configs: [ + { + column: 'main__created_at', + query: 'main.created_at', + label: 'Created Date', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.DATE_EPOCH, + }, + { + column: 'main__update_at', + query: 'main.update_at', + label: 'Updated Date', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.DATE_EPOCH, + }, + { + column: 'main__name', + query: 'main.name', + label: 'Name', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + ], + filter_configs: [], +}; diff --git a/src/modules/reports/shared/configs/general-report/index.ts b/src/modules/reports/shared/configs/general-report/index.ts index 9d6a412..1bba05d 100644 --- a/src/modules/reports/shared/configs/general-report/index.ts +++ b/src/modules/reports/shared/configs/general-report/index.ts @@ -1 +1,5 @@ -export const GeneralReportConfig = []; +import { ReportConfigEntity } from '../../entities/report-config.entity'; + +import SampleReport from './configs/sample.report'; + +export const GeneralReportConfig: ReportConfigEntity[] = [SampleReport]; diff --git a/src/modules/reports/shared/configs/index.ts b/src/modules/reports/shared/configs/index.ts index 1ec9aab..603161a 100644 --- a/src/modules/reports/shared/configs/index.ts +++ b/src/modules/reports/shared/configs/index.ts @@ -1,4 +1,8 @@ +import { ReportConfigEntity } from '../entities/report-config.entity'; import { GeneralReportConfig } from './general-report'; import { TenantReportConfig } from './tenant-report'; -export const ReportConfigs = [...GeneralReportConfig, ...TenantReportConfig]; +export const ReportConfigs: ReportConfigEntity[] = [ + ...GeneralReportConfig, + ...TenantReportConfig, +]; diff --git a/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts b/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts new file mode 100644 index 0000000..2c223fe --- /dev/null +++ b/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts @@ -0,0 +1,39 @@ +import { DATA_FORMAT, DATA_TYPE, REPORT_GROUP } from '../../../constant'; +import { ReportConfigEntity } from '../../../entities/report-config.entity'; + +export default { + group_name: REPORT_GROUP.tenant_report, + unique_name: `${REPORT_GROUP.tenant_report}__sample`, + label: 'Sample Tenant Report ', + table_schema: 'season_types', + main_table_alias: 'main', + defaultOrderBy: [], + lowLevelOrderBy: [], + filter_period_config: { + hidden: true, + }, + column_configs: [ + { + column: 'main__created_at', + query: 'main.created_at', + label: 'Created Date', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.DATE_EPOCH, + }, + { + column: 'main__update_at', + query: 'main.update_at', + label: 'Updated Date', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.DATE_EPOCH, + }, + { + column: 'main__name', + query: 'main.name', + label: 'Name', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + ], + filter_configs: [], +}; diff --git a/src/modules/reports/shared/configs/tenant-report/index.ts b/src/modules/reports/shared/configs/tenant-report/index.ts index 3893e35..dfe07b1 100644 --- a/src/modules/reports/shared/configs/tenant-report/index.ts +++ b/src/modules/reports/shared/configs/tenant-report/index.ts @@ -1 +1,4 @@ -export const TenantReportConfig = []; +import { ReportConfigEntity } from '../../entities/report-config.entity'; +import SampleReport from './configs/sample.report'; + +export const TenantReportConfig: ReportConfigEntity[] = [SampleReport]; diff --git a/src/modules/reports/shared/constant/report-group.constant.ts b/src/modules/reports/shared/constant/report-group.constant.ts index 80476fa..3fc0b60 100644 --- a/src/modules/reports/shared/constant/report-group.constant.ts +++ b/src/modules/reports/shared/constant/report-group.constant.ts @@ -1,5 +1,6 @@ export enum REPORT_GROUP { // PATTERN => MODULE__MENU__SUB_MENU // EXAMPLE: - contact__reports = 'contact__reports', + general_report = 'general_report', + tenant_report = 'tenant_report', } diff --git a/src/modules/reports/shared/entities/report-config.entity.ts b/src/modules/reports/shared/entities/report-config.entity.ts index 90dc77f..bca0f5c 100644 --- a/src/modules/reports/shared/entities/report-config.entity.ts +++ b/src/modules/reports/shared/entities/report-config.entity.ts @@ -55,5 +55,5 @@ export interface ReportConfigEntity { filter_configs?: FilterConfigEntity[]; filter_period_config?: FilterPeriodConfigEntity; ignore_filter_keys?: string[]; - customQueryFilter?(column: string): string; + customQueryColumn?(column: string): string; } diff --git a/src/modules/reports/shared/helpers/query-builder.ts b/src/modules/reports/shared/helpers/query-builder.ts index 458bfe1..825b76f 100644 --- a/src/modules/reports/shared/helpers/query-builder.ts +++ b/src/modules/reports/shared/helpers/query-builder.ts @@ -8,6 +8,7 @@ import { export class ReportQueryBuilder { public reportConfig: ReportConfigEntity; public queryModel: QueryModelEntity; + constructor(reportConfig: ReportConfigEntity, queryModel: QueryModelEntity) { this.reportConfig = reportConfig; this.queryModel = queryModel; @@ -16,53 +17,73 @@ export class ReportQueryBuilder { getBaseConfig() { const tableSchema = this.reportConfig.table_schema; const mainTableAlias = this.reportConfig.main_table_alias ?? 'main'; - const queryModel = this.queryModel; + + 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, - queryModel, + selectSql, + selectSqlExport, + whereSql, + limitSql, + orderBySql, + orderBySqlExport, + ...groupBy, + ...groupByExport, }; } - // OLD VERSION - buildSql(reportConfig: ReportConfigEntity, queryModel: any) { - const tableSchema = reportConfig.table_schema; - const mainTableAlias = reportConfig.main_table_alias ?? 'main'; + getSql(): string { + const { + selectSql, + tableSchema, + whereSql, + groupByQuery, + orderBySql, + limitSql, + } = this.getBaseConfig(); + return `SELECT ${selectSql} FROM ${tableSchema} ${whereSql} ${groupByQuery} ${orderBySql} ${limitSql}` as string; + } - const selectSql = this.createSelectSql(queryModel, reportConfig); - const selectSqlExport = this.createSelectSqlExport( - queryModel, - reportConfig, - ); + getSqlCount(): string { + const { groupByColumn, mainTableAlias, tableSchema, whereSql } = + this.getBaseConfig(); - const whereSql = this.createWhereSql(queryModel, reportConfig); - const limitSql = this.createLimitSql(queryModel); - - const orderBySql = this.createOrderBySql(queryModel, reportConfig); - const orderBySqlExport = this.createOrderBySqlExport( - queryModel, - reportConfig, - ); - - const { groupByQuery, groupByColumn } = this.createGroupBySql( - queryModel, - reportConfig, - ); - const { groupByQueryExport, groupByColumnExport } = - this.createGroupBySqlExport(queryModel, reportConfig); - - const SQL: string = - `SELECT ${selectSql} FROM ${tableSchema} ${whereSql} ${groupByQuery} ${orderBySql} ${limitSql}` as string; - const SQL_COUNT: string = `SELECT COUNT(${ + 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; - const SQL_EXPORT: string = - `SELECT ${selectSqlExport} FROM ${tableSchema} ${whereSql} ${groupByQueryExport} ${orderBySqlExport}` as string; - const SQL_EXPORT_COUNT: string = `SELECT COUNT(${ + } + + 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` @@ -71,17 +92,70 @@ export class ReportQueryBuilder { ? `+ COUNT(DISTINCT CASE WHEN ${groupByColumnExport} IS NULL THEN 1 END)` : '' } AS count FROM ${tableSchema} ${whereSql}` as string; - - return { SQL, SQL_COUNT, SQL_EXPORT, SQL_EXPORT_COUNT }; } - createSelectSql(queryModel: any, reportConfig: ReportConfigEntity): string { - const configColumns = reportConfig.column_configs; - const mainTableAlias = reportConfig.main_table_alias ?? 'main'; + 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; + } - const rowGroupCols = queryModel.rowGroupCols; - const valueCols = queryModel.valueCols; - const groupKeys = queryModel.groupKeys; + 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 = []; @@ -89,14 +163,12 @@ export class ReportQueryBuilder { const rowGroupColChildCount = rowGroupCols[groupKeys.length + 1]; colsToSelect.push( - this.findQueryConfig(reportConfig, rowGroupCol.field) + - ` AS ${rowGroupCol.field}`, + this.findQueryConfig(rowGroupCol.field) + ` AS ${rowGroupCol.field}`, ); if (rowGroupColChildCount) { colsToSelect.push( `COUNT(DISTINCT ${this.findQueryConfig( - reportConfig, rowGroupColChildCount.field, )}) AS count_child_group`, ); @@ -109,7 +181,6 @@ export class ReportQueryBuilder { valueCols.forEach(function (valueCol) { colsToSelect.push( `${valueCol.aggFunc} (${thisSelf.findQueryConfig( - reportConfig, valueCol.field, )}) AS ${valueCol.field}`, ); @@ -124,14 +195,11 @@ export class ReportQueryBuilder { return columns.join(', '); } - createSelectSqlExport( - queryModel: any, - reportConfig: ReportConfigEntity, - ): string { - const configColumns = reportConfig.column_configs; + createSelectSqlExport(): string { + const configColumns = this.reportConfig.column_configs; - const rowGroupCols = queryModel.rowGroupCols; - const valueCols = queryModel.valueCols; + const rowGroupCols = this.queryModel.rowGroupCols; + const valueCols = this.queryModel.valueCols; // eslint-disable-next-line @typescript-eslint/no-this-alias const thisSelf = this; @@ -140,7 +208,7 @@ export class ReportQueryBuilder { const colsToSelect = []; rowGroupCols.forEach(function (rowGroupCol) { colsToSelect.push( - thisSelf.findQueryConfig(reportConfig, rowGroupCol.field) + + thisSelf.findQueryConfig(rowGroupCol.field) + ` AS ${rowGroupCol.field}`, ); }); @@ -148,7 +216,6 @@ export class ReportQueryBuilder { valueCols.forEach(function (valueCol) { colsToSelect.push( `${valueCol.aggFunc} (${thisSelf.findQueryConfig( - reportConfig, valueCol.field, )}) AS ${valueCol.field}`, ); @@ -161,7 +228,9 @@ export class ReportQueryBuilder { return columns.join(', '); } + // ================================================================== + // GENERATE WHERE QUERY ============================================= createFilterSql(column: string, item: { type: FILTER_TYPE; filter: any }) { switch (item.type) { // TEXT @@ -237,23 +306,22 @@ export class ReportQueryBuilder { } } - createWhereSql(queryModel, reportConfig: ReportConfigEntity) { - // const configColumns = reportConfig.column_configs ?? []; - const configFilters = reportConfig.filter_configs ?? []; - const ignoreFilterKeys = reportConfig.ignore_filter_keys ?? []; + 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 = reportConfig?.whereCondition - ? reportConfig.whereCondition(queryModel.filterModel) + const whereCondition = this.reportConfig?.whereCondition + ? this.reportConfig.whereCondition(this.queryModel.filterModel) : []; - const rowGroupCols = queryModel.rowGroupCols; - const groupKeys = queryModel.groupKeys; - const filterModel = 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; @@ -263,9 +331,7 @@ export class ReportQueryBuilder { groupKeys.forEach(function (key, index) { const colName = rowGroupCols[index].field; // whereParts.push(colName + ' = "' + key + '"'); - whereParts.push( - `${thisSelf.findQueryConfig(reportConfig, colName)} = '${key}'`, - ); + whereParts.push(`${thisSelf.findQueryConfig(colName)} = '${key}'`); }); } @@ -274,14 +340,14 @@ export class ReportQueryBuilder { keySet.forEach(function (key) { if (!ignoreFilterKey.includes(key)) { const item = filterModel[key]; - const newKey = thisSelf.findQueryConfig(reportConfig, key); + const newKey = thisSelf.findQueryConfig(key); whereParts.push(thisSelf.createFilterSql(newKey, item)); } }); } // set default where conditions - const defaultConditions = reportConfig.whereDefaultConditions; + const defaultConditions = this.reportConfig.whereDefaultConditions; const defaultWhereOptions = []; if (defaultConditions) { defaultConditions.forEach((condition) => { @@ -309,15 +375,17 @@ export class ReportQueryBuilder { return ''; } } + // ================================================================== - createOrderBySql(queryModel, reportConfig: ReportConfigEntity) { - const mainTableAlias = reportConfig.main_table_alias ?? 'main'; - const defaultOrderBy = reportConfig.defaultOrderBy ?? []; - const lowLevelOrderBy = reportConfig.lowLevelOrderBy ?? []; + // GENERATE ORDER QUERY ============================================= + createOrderBySql() { + const mainTableAlias = this.reportConfig.main_table_alias ?? 'main'; + const defaultOrderBy = this.reportConfig.defaultOrderBy ?? []; + const lowLevelOrderBy = this.reportConfig.lowLevelOrderBy ?? []; - const rowGroupCols = queryModel.rowGroupCols; - const groupKeys = queryModel.groupKeys; - const sortModel = queryModel.sortModel; + const rowGroupCols = this.queryModel.rowGroupCols; + const groupKeys = this.queryModel.groupKeys; + const sortModel = this.queryModel.sortModel; const grouping = this.isDoingGrouping(rowGroupCols, groupKeys); @@ -370,13 +438,13 @@ export class ReportQueryBuilder { } } - createOrderBySqlExport(queryModel, reportConfig: ReportConfigEntity) { - const mainTableAlias = reportConfig.main_table_alias ?? 'main'; - const defaultOrderBy = reportConfig.defaultOrderBy ?? []; - const lowLevelOrderBy = reportConfig.lowLevelOrderBy ?? []; + createOrderBySqlExport() { + const mainTableAlias = this.reportConfig.main_table_alias ?? 'main'; + const defaultOrderBy = this.reportConfig.defaultOrderBy ?? []; + const lowLevelOrderBy = this.reportConfig.lowLevelOrderBy ?? []; - const rowGroupCols = queryModel.rowGroupCols; - const sortModel = queryModel.sortModel; + const rowGroupCols = this.queryModel.rowGroupCols; + const sortModel = this.queryModel.sortModel; const defaultOrder = defaultOrderBy[0] ? defaultOrderBy @@ -400,17 +468,18 @@ export class ReportQueryBuilder { ); } } + // ================================================================== - createGroupBySql(queryModel, reportConfig: ReportConfigEntity) { - // const configColumns = reportConfig.column_configs; - const rowGroupCols = queryModel.rowGroupCols; - const groupKeys = queryModel.groupKeys; + // 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(reportConfig, rowGroupCol.field)); + colsToGroupBy.push(this.findQueryConfig(rowGroupCol.field)); return { groupByQuery: ' GROUP BY ' + colsToGroupBy.join(', '), @@ -424,9 +493,8 @@ export class ReportQueryBuilder { } } - createGroupBySqlExport(queryModel, reportConfig: ReportConfigEntity) { - // const configColumns = reportConfig.column_configs; - const rowGroupCols = queryModel.rowGroupCols; + createGroupBySqlExport() { + const rowGroupCols = this.queryModel.rowGroupCols; // eslint-disable-next-line @typescript-eslint/no-this-alias const thisSelf = this; @@ -434,9 +502,7 @@ export class ReportQueryBuilder { if (rowGroupCols.length > 0) { const colsToGroupBy = []; rowGroupCols.forEach(function (rowGroupCol) { - colsToGroupBy.push( - thisSelf.findQueryConfig(reportConfig, rowGroupCol.field), - ); + colsToGroupBy.push(thisSelf.findQueryConfig(rowGroupCol.field)); }); return { @@ -450,57 +516,5 @@ export class ReportQueryBuilder { }; } } - - findQueryConfig(config: ReportConfigEntity, column: string) { - const configColumns = config.column_configs ?? []; - - const findQuery: ReportColumnConfigEntity = configColumns.find( - (el) => el.column === column, - ); - const customQuery = config?.customQueryFilter - ? config.customQueryFilter(column) - : undefined; - - if (customQuery) return customQuery; - else if (findQuery) return findQuery.query; - - return column.replace('__', '.'); - } - - 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(queryModel) { - const startRow = queryModel.startRow; - const endRow = queryModel.endRow; - const pageSize = endRow - startRow; - return ' LIMIT ' + (pageSize + 1) + ' OFFSET ' + startRow; - } - - getRowCount(queryModel, results) { - if (results === null || results === undefined || results.length === 0) { - return null; - } - const currentLastRow = queryModel.startRow + results.length; - return currentLastRow <= queryModel.endRow ? currentLastRow : -1; - } - - cutResultsToPageSize(queryModel, results) { - const pageSize = queryModel.endRow - queryModel.startRow; - if (results && results.length > pageSize) { - return results.splice(0, pageSize); - } else { - return results; - } - } + // ================================================================== } -- 2.40.1 From 85d461c70a59ff7b128659b5ac2255957c7fbe3b Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:05:36 +0700 Subject: [PATCH 3/4] feat: setup service get report config --- src/modules/reports/report/report.service.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/modules/reports/report/report.service.ts b/src/modules/reports/report/report.service.ts index 0474f56..c494e32 100644 --- a/src/modules/reports/report/report.service.ts +++ b/src/modules/reports/report/report.service.ts @@ -7,12 +7,26 @@ import { ReportConfigs } from '../shared/configs'; @Injectable() export class ReportService extends BaseReportService { async getReportConfig(query: GetReportConfigDto) { - return ReportConfigs; + const { unique_names = [], group_names = [] } = query; + + let configs = ReportConfigs; + if (group_names.length > 0) { + configs = configs.filter((item) => group_names.includes(item.group_name)); + } + + if (unique_names.length > 0) { + configs = configs.filter((item) => + unique_names.includes(item.unique_name), + ); + } + + return configs; } async getReportData(body: GetReportDataDto) { return 'you hit API for get report data'; } + async getReportMeta(body: GetReportDataDto) { return 'you hit API for get report meta'; } -- 2.40.1 From fc37e0c502a37ba8f005bf0385202ea8b5b25ccd Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:03:13 +0700 Subject: [PATCH 4/4] feat: integration API for get data and get meta data --- .../report-bookmark/report-bookmark.module.ts | 8 +- .../report-bookmark.service.ts | 32 ++++- .../report-export/report-export.module.ts | 11 +- .../report-export/report-export.service.ts | 14 ++ src/modules/reports/report/report.module.ts | 10 +- src/modules/reports/report/report.service.ts | 123 +++++++++++++++++- .../general-report/configs/sample.report.ts | 6 +- .../tenant-report/configs/sample.report.ts | 6 +- .../reports/shared/dto/report-data.get.dto.ts | 24 +++- .../shared/helpers/rounding-currency.ts | 4 + 10 files changed, 223 insertions(+), 15 deletions(-) create mode 100644 src/modules/reports/shared/helpers/rounding-currency.ts diff --git a/src/modules/reports/report-bookmark/report-bookmark.module.ts b/src/modules/reports/report-bookmark/report-bookmark.module.ts index 4a3a2a8..ed44501 100644 --- a/src/modules/reports/report-bookmark/report-bookmark.module.ts +++ b/src/modules/reports/report-bookmark/report-bookmark.module.ts @@ -1,9 +1,15 @@ import { Module } from '@nestjs/common'; import { ReportBookmarkController } from './report-bookmark.controller'; import { ReportBookmarkService } from './report-bookmark.service'; +import { ConfigModule } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { ReportBookmarkModel } from '../shared/models/report-bookmark.model'; @Module({ - imports: [], + imports: [ + TypeOrmModule.forFeature([ReportBookmarkModel], CONNECTION_NAME.DEFAULT), + ], controllers: [ReportBookmarkController], providers: [ReportBookmarkService], }) diff --git a/src/modules/reports/report-bookmark/report-bookmark.service.ts b/src/modules/reports/report-bookmark/report-bookmark.service.ts index 4f76e2b..7cbe05f 100644 --- a/src/modules/reports/report-bookmark/report-bookmark.service.ts +++ b/src/modules/reports/report-bookmark/report-bookmark.service.ts @@ -5,15 +5,45 @@ import { GetLabelReportBookmarkDto, GetReportBookmarkDto, } from '../shared/dto/report-bookmark.get.dto'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { ReportBookmarkModel } from '../shared/models/report-bookmark.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; @Injectable() export class ReportBookmarkService extends BaseReportService { + constructor( + @InjectRepository(ReportBookmarkModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + ) { + super(); + } + async create(body: CreateReportBookmarkDto) { return 'you hit API for create report bookmark'; } async getAll(query: GetReportBookmarkDto) { - return 'you hit API for get all report bookmark'; + const modelName = ReportBookmarkModel.name; + + const requestor_id = this.userProvider.user.id; + const unique_names = query.unique_names; + const group_names = query.group_names; + + const qb = this.repo + .createQueryBuilder(modelName) + .where((query) => { + if (unique_names) { + query.andWhere(`unique_name IN (:...unique_names)`, { unique_names }); + } + if (group_names) { + query.andWhere(`group_name =IN (:...group_names)`, { group_names }); + } + query.andWhere(`requestor_id = :requestor_id`, { requestor_id }); + }) + .orderBy(`${modelName}.created_at`, 'DESC'); + + return await qb.getMany(); } async getAllLabelHistory(query: GetLabelReportBookmarkDto) { diff --git a/src/modules/reports/report-export/report-export.module.ts b/src/modules/reports/report-export/report-export.module.ts index bbd3269..0506d59 100644 --- a/src/modules/reports/report-export/report-export.module.ts +++ b/src/modules/reports/report-export/report-export.module.ts @@ -1,9 +1,18 @@ import { Module } from '@nestjs/common'; import { ReportExportController } from './report-export.controller'; import { ReportExportService } from './report-export.service'; +import { ConfigModule } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ExportReportHistoryModel } from '../shared/models/export-report-history.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; @Module({ - imports: [], + imports: [ + TypeOrmModule.forFeature( + [ExportReportHistoryModel], + CONNECTION_NAME.DEFAULT, + ), + ], controllers: [ReportExportController], providers: [ReportExportService], }) diff --git a/src/modules/reports/report-export/report-export.service.ts b/src/modules/reports/report-export/report-export.service.ts index c5b6322..1586329 100644 --- a/src/modules/reports/report-export/report-export.service.ts +++ b/src/modules/reports/report-export/report-export.service.ts @@ -6,9 +6,23 @@ import { GetReportExportFileNameDto, GetReportExportProcessingDto, } from '../shared/dto/report-export.get.dto'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { ExportReportHistoryModel } from '../shared/models/export-report-history.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { DataSource, Repository } from 'typeorm'; @Injectable() export class ReportExportService extends BaseReportService { + constructor( + @InjectDataSource(CONNECTION_NAME.DEFAULT) + private dataSource: DataSource, + + @InjectRepository(ExportReportHistoryModel, CONNECTION_NAME.DEFAULT) + private exportHistoryRepo: Repository, + ) { + super(); + } + async create(body: CreateReportExportDto) { return 'you hit API for create report export'; } diff --git a/src/modules/reports/report/report.module.ts b/src/modules/reports/report/report.module.ts index b624fe0..805a2d1 100644 --- a/src/modules/reports/report/report.module.ts +++ b/src/modules/reports/report/report.module.ts @@ -1,9 +1,17 @@ import { Module } from '@nestjs/common'; import { ReportController } from './report.controller'; import { ReportService } from './report.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ExportReportHistoryModel } from '../shared/models/export-report-history.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; @Module({ - imports: [], + imports: [ + TypeOrmModule.forFeature( + [ExportReportHistoryModel], + CONNECTION_NAME.DEFAULT, + ), + ], controllers: [ReportController], providers: [ReportService], }) diff --git a/src/modules/reports/report/report.service.ts b/src/modules/reports/report/report.service.ts index c494e32..9bcb8fe 100644 --- a/src/modules/reports/report/report.service.ts +++ b/src/modules/reports/report/report.service.ts @@ -1,11 +1,27 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { BaseReportService } from '../shared/services/base-report.service'; import { GetReportConfigDto } from '../shared/dto/report-config.get.dto'; import { GetReportDataDto } from '../shared/dto/report-data.get.dto'; import { ReportConfigs } from '../shared/configs'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { DataSource } from 'typeorm'; +import { ReportConfigEntity } from '../shared/entities/report-config.entity'; +import { ReportQueryBuilder } from '../shared/helpers'; +import { DATA_FORMAT } from '../shared/constant'; +import { roundingCurrency } from '../shared/helpers/rounding-currency'; @Injectable() export class ReportService extends BaseReportService { + private readonly logger = new Logger(ReportService.name); + + constructor( + @InjectDataSource(CONNECTION_NAME.DEFAULT) + private dataSource: DataSource, + ) { + super(); + } + async getReportConfig(query: GetReportConfigDto) { const { unique_names = [], group_names = [] } = query; @@ -23,11 +39,112 @@ export class ReportService extends BaseReportService { return configs; } + getReportConfigByUniqueName(unique_name): ReportConfigEntity { + return ReportConfigs.find((item) => item.unique_name === unique_name); + } + async getReportData(body: GetReportDataDto) { - return 'you hit API for get report data'; + try { + const queryModel = body.query_model; + const reportConfig = this.getReportConfigByUniqueName(body.unique_name); + const builder = new ReportQueryBuilder(reportConfig, queryModel); + const SQL = builder.getSql(); + const queryResult = await this.dataSource.query(SQL); + + const realData = []; + const configColumns = reportConfig.column_configs; + + for (const item of queryResult) { + const realItem = {}; + for (const itemKey of Object.keys(item)) { + if (itemKey === 'count_child_group') { + const realValue = item[itemKey] ?? 0; + Object.assign(realItem, { [`${itemKey}`]: Number(realValue) }); + } else { + const confCol = configColumns.find((c) => c.column === itemKey); + const isNumber = confCol.format === DATA_FORMAT.NUMBER; + const isCurrency = confCol.format === DATA_FORMAT.CURRENCY; + const isMinusCurrency = + confCol.format === DATA_FORMAT.MINUS_CURRENCY; + const isBoolean = confCol.format === DATA_FORMAT.BOOLEAN; + const isPercentage = confCol.format === DATA_FORMAT.PERCENTAGE; + const isSetInitNull = isNumber || isCurrency || isMinusCurrency; + const isTextUpperCase = + confCol.format === DATA_FORMAT.TEXT_UPPERCASE; + const isTextLowerCase = + confCol.format === DATA_FORMAT.TEXT_LOWERCASE; + + if (isSetInitNull) { + const realValue = item[itemKey] ?? 0; + if (isCurrency) { + Object.assign(realItem, { + [`${itemKey}`]: roundingCurrency(realValue), + }); + } else if (isMinusCurrency) { + Object.assign(realItem, { + [`${itemKey}`]: roundingCurrency(realValue) * -1, + }); + } else { + Object.assign(realItem, { [`${itemKey}`]: realValue }); + } + } else if (isPercentage) { + const realValue = item[itemKey] + ? `${item[itemKey]}%` + : item[itemKey]; + Object.assign(realItem, { [`${itemKey}`]: realValue }); + } else if (isBoolean) { + let realValue = ''; + if (item[itemKey] === true || item[itemKey] === 1) + realValue = 'Yes'; + else if (item[itemKey] === false || item[itemKey] === 0) + realValue = 'No'; + Object.assign(realItem, { [`${itemKey}`]: realValue }); + } else if (isTextUpperCase) { + Object.assign(realItem, { + [`${itemKey}`]: item[itemKey]?.toUpperCase(), + }); + } else if (isTextLowerCase) { + Object.assign(realItem, { + [`${itemKey}`]: item[itemKey]?.toLowerCase(), + }); + } else { + Object.assign(realItem, { [`${itemKey}`]: item[itemKey] }); + } + } + } + realData.push(realItem); + } + + return realData; + } catch (error) { + this.logger.error(error); + throw error; + } } async getReportMeta(body: GetReportDataDto) { - return 'you hit API for get report meta'; + try { + const queryModel = body.query_model; + const reportConfig = this.getReportConfigByUniqueName(body.unique_name); + const builder = new ReportQueryBuilder(reportConfig, queryModel); + const SQL_COUNT = builder.getSqlCount(); + const queryResult = await this.dataSource.query(SQL_COUNT); + + const totalRow = parseInt(queryResult[0].count); + const startRow = queryModel.startRow; + const endRow = queryModel.endRow; + const pageSize = endRow - startRow; + + const meta = { + total_row: totalRow, + limit: pageSize, + offset: startRow, + }; + + return meta; + } catch (error) { + this.logger.error(error); + throw error; + } } } diff --git a/src/modules/reports/shared/configs/general-report/configs/sample.report.ts b/src/modules/reports/shared/configs/general-report/configs/sample.report.ts index cabbd03..a832ba5 100644 --- a/src/modules/reports/shared/configs/general-report/configs/sample.report.ts +++ b/src/modules/reports/shared/configs/general-report/configs/sample.report.ts @@ -5,7 +5,7 @@ export default { group_name: REPORT_GROUP.general_report, unique_name: `${REPORT_GROUP.general_report}__sample`, label: 'Sample General Report ', - table_schema: 'season_types', + table_schema: 'season_types main', main_table_alias: 'main', defaultOrderBy: [], lowLevelOrderBy: [], @@ -22,8 +22,8 @@ export default { format: DATA_FORMAT.DATE_EPOCH, }, { - column: 'main__update_at', - query: 'main.update_at', + column: 'main__updated_at', + query: 'main.updated_at', label: 'Updated Date', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.DATE_EPOCH, diff --git a/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts b/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts index 2c223fe..62f2871 100644 --- a/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts +++ b/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts @@ -5,7 +5,7 @@ export default { group_name: REPORT_GROUP.tenant_report, unique_name: `${REPORT_GROUP.tenant_report}__sample`, label: 'Sample Tenant Report ', - table_schema: 'season_types', + table_schema: 'season_types main', main_table_alias: 'main', defaultOrderBy: [], lowLevelOrderBy: [], @@ -21,8 +21,8 @@ export default { format: DATA_FORMAT.DATE_EPOCH, }, { - column: 'main__update_at', - query: 'main.update_at', + column: 'main__updated_at', + query: 'main.updated_at', label: 'Updated Date', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.DATE_EPOCH, diff --git a/src/modules/reports/shared/dto/report-data.get.dto.ts b/src/modules/reports/shared/dto/report-data.get.dto.ts index a66b84b..b33da70 100644 --- a/src/modules/reports/shared/dto/report-data.get.dto.ts +++ b/src/modules/reports/shared/dto/report-data.get.dto.ts @@ -1,13 +1,22 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsObject, IsString, ValidateIf } from 'class-validator'; import { QueryModelEntity } from '../entities/query-model.entity'; +import { REPORT_GROUP } from '../constant'; export class GetReportDataDto { - @ApiProperty({ name: 'group_name', required: true }) + @ApiProperty({ + name: 'group_name', + required: true, + default: REPORT_GROUP.general_report, + }) @IsString() group_name: string; - @ApiProperty({ name: 'unique_name', required: true }) + @ApiProperty({ + name: 'unique_name', + required: true, + default: `${REPORT_GROUP.general_report}__sample`, + }) @IsString() unique_name: string; @@ -15,6 +24,17 @@ export class GetReportDataDto { name: 'query_model', type: Object, required: true, + default: { + startRow: 0, + endRow: 100, + rowGroupCols: [], + valueCols: [], + pivotCols: [], + pivotMode: true, + groupKeys: [], + filterModel: {}, + sortModel: [], + }, }) @IsObject() @ValidateIf((body) => body.query_model) diff --git a/src/modules/reports/shared/helpers/rounding-currency.ts b/src/modules/reports/shared/helpers/rounding-currency.ts new file mode 100644 index 0000000..323239b --- /dev/null +++ b/src/modules/reports/shared/helpers/rounding-currency.ts @@ -0,0 +1,4 @@ +export function roundingCurrency(value) { + if (!value) return value; + return Number(value).toFixed(2); +} -- 2.40.1