@isoftdata/utility-dashboard-backend 2.0.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/chart-helper.js DELETED
@@ -1,268 +0,0 @@
1
- const { formatterTemplates } = require('./formatter-templates.json')
2
- const toSnakeCase = require('to-snake-case')
3
- const mysqlDataTypes = require('mysql/lib/protocol/constants/types')
4
- const klona = require('klona')
5
- const styleTableRow = require('./style-templates.js')
6
-
7
- const mysqlToGoogleChartsDataTypeMap = mysqlDataType => {
8
- switch (mysqlDataType) {
9
- case 'VAR_STRING':
10
- case 'STRING':
11
- case 'ENUM':
12
- case 'VARCHAR':
13
- return 'string'
14
- case 'DECIMAL':
15
- case 'NEWDECIMAL':
16
- case 'BIT':
17
- case 'TINY':
18
- case 'SHORT':
19
- case 'LONG':
20
- case 'FLOAT':
21
- case 'DOUBLE':
22
- case 'LONGLONG':
23
- case 'INT24':
24
- case 'YEAR':
25
- return 'number'
26
- case 'TIMESTAMP':
27
- case 'DATETIME':
28
- case 'TIMESTAMP2':
29
- case 'DATETIME2':
30
- return 'datetime'
31
- case 'DATE':
32
- case 'NEWDATE':
33
- return 'date'
34
- case 'TIME':
35
- case 'TIME2':
36
- return 'timeofday'
37
- default:
38
- return 'string'
39
- }
40
- }
41
-
42
- const makeDataTableCol = (key, fields, role = null) => {
43
- return {
44
- id: toSnakeCase(key).toLowerCase(),
45
- label: key,
46
- type: mysqlToGoogleChartsDataTypeMap(fields[key]),
47
- role,
48
- }
49
- }
50
-
51
- module.exports = {
52
- queryWithFields: (connection, query) => { // the only function here that talks to the server at all
53
- return new Promise((resolve, reject) => {
54
- connection.query(query, (err, results, fields) => {
55
- if (err) {
56
- console.error('Error running query for dashboard chart', err)
57
- reject(err)
58
- } else {
59
- resolve({
60
- results,
61
- fields: fields.reduce((sum, field) => {
62
- return {
63
- ...sum,
64
- [field.name]: mysqlDataTypes[field.type] || field.type,
65
- }
66
- }, {}),
67
- })
68
- }
69
- })
70
- })
71
- },
72
- getMultiSeriesData: ({ data: queryResult, seriesOptions, formatting, fields }) => {
73
- const { groupXAxisBy, series, value, tooltip } = seriesOptions
74
- //Get an array of all of the keys for the series
75
- const allOfTheKeysInTheSeries = new Set(queryResult.map(row => row[series]))
76
-
77
- let objectOfAllKeysWithNullValues = {}
78
- for (const key of allOfTheKeysInTheSeries) {
79
- objectOfAllKeysWithNullValues[key] = null
80
- //add in a spot for the tooltips for each column to go
81
- if (tooltip) {
82
- objectOfAllKeysWithNullValues[`${tooltip} ${key}`] = null
83
- }
84
- }
85
-
86
- let grouped = {}
87
-
88
- //create objects for each thing they're grouping by
89
- // console.time(`create objects for each thing they're grouping by`)
90
- for (const row of queryResult) {
91
- //if we don't yet have a prop for what we're grouping by, then we need to initialize an object of all null values for it
92
- if (!grouped[row[groupXAxisBy]]) {
93
- grouped[row[groupXAxisBy]] = klona(objectOfAllKeysWithNullValues) //give nulls for all values by default
94
- }
95
-
96
- grouped[row[groupXAxisBy]][row[series]] = row[value]
97
- // add the tooltips for each series in each row
98
- if (tooltip) {
99
- grouped[row[groupXAxisBy]][`${tooltip} ${row[series]}`] = row[tooltip]
100
- }
101
- }
102
- // console.timeEnd(`create objects for each thing they're grouping by`)
103
-
104
- //The mysql field types need to map to our output data
105
- let seriesFields = { [groupXAxisBy]: fields[groupXAxisBy] }
106
- let seriesFormatting = {}
107
- for (const key of allOfTheKeysInTheSeries) {
108
- seriesFields[key] = fields[value]
109
- //add tooltips here, so a tooltip column is created for every series column
110
- if (tooltip) {
111
- seriesFields[`${tooltip} ${key}`] = fields[tooltip]
112
- }
113
- //If there is any formatting applied to the value, we need to apply it to all of the other keys
114
- if (formatting && value in formatting) {
115
- seriesFormatting[key] = formatting[value]
116
- }
117
- }
118
-
119
- //turn it back to an array and make sure the grouping column is first as required by a pie chart(at least)
120
- // console.time(`group it back together`)
121
- let multiSeriesData = []
122
- for (const key in grouped) {
123
- multiSeriesData.push({
124
- [groupXAxisBy]: key,
125
- ...grouped[key],
126
- })
127
- }
128
- // console.timeEnd(`group it back together`)
129
- // console.log(`${multiSeriesData.length} rows with ${Object.keys(multiSeriesData[0]).length} props each.`)
130
- return {
131
- data: multiSeriesData,
132
- formatting: {
133
- ...formatting,
134
- ...seriesFormatting,
135
- },
136
- fields: seriesFields,
137
- }
138
- },
139
- makeNegativeNumbericValuesZero: resultSet => {
140
- const numberCols = Object.keys(resultSet[0]).filter(key => typeof resultSet[0][key] === 'number')
141
-
142
- return resultSet.map(row => {
143
- numberCols.forEach(col => {
144
- if (row[col] < 0) {
145
- row[col] = 0
146
- }
147
- })
148
-
149
- return row
150
- })
151
- },
152
- coerceNullColumn: (columnProp, data, destinationType) => {
153
- const hasNullValue = data.find(row => row[columnProp] === null)
154
-
155
- if (hasNullValue) {
156
- const rowWithValidValue = data.find(row => row[columnProp] !== null)
157
- let nullReplacement = ''
158
-
159
- if ((destinationType && destinationType === 'number') || (rowWithValidValue && typeof rowWithValidValue[columnProp] === 'number')) {
160
- nullReplacement = 0
161
- }
162
-
163
- return data.map(row => {
164
- return {
165
- ...row,
166
- [columnProp]: row[columnProp] ?? nullReplacement,
167
- }
168
- })
169
- } else {
170
- return data
171
- }
172
- },
173
- handleFormattingTemplates: formatting => {
174
- if (formatting && typeof formatting === 'object') {
175
- for (const key in formatting) {
176
- if (typeof formatting[key].format === 'string') {
177
- if (formatting[key].format in formatterTemplates) {
178
- formatting[key].format = formatterTemplates[formatting[key].format]
179
- } else {
180
- formatting[key].format = {}
181
- }
182
- }
183
- }
184
- }
185
-
186
- return formatting
187
- },
188
- getDataTableFormat: (data, fields, multiSeries) => {
189
- let cols = []
190
- // domain column for multiseries charts
191
- if (multiSeries) {
192
- cols = [ makeDataTableCol(multiSeries.groupXAxisBy, fields) ]
193
- }
194
- // All other columns (including domain for non multiseries charts)
195
- for (const key in fields) {
196
- if (!multiSeries || key !== multiSeries.groupXAxisBy) {
197
- if (multiSeries && multiSeries.tooltip && key.includes(multiSeries.tooltip)) {
198
- // If it's a tooltip, set the column's role exlicitly
199
- cols.push(makeDataTableCol(key, fields, 'tooltip'))
200
- } else {
201
- // Otherwise, role is implicitly assigned
202
- cols.push(makeDataTableCol(key, fields))
203
- }
204
- }
205
- }
206
-
207
- const rows = data.map(row => {
208
- let items = []
209
-
210
- for (const column in row) {
211
- items.push({ v: row[column] })
212
- }
213
-
214
- return {
215
- c: items,
216
- }
217
- })
218
-
219
- return { cols, rows }
220
- },
221
- /**
222
- * Replace all null values with the previous non-null value, and optionally make data cumulative
223
- * @param {Object} dataTable - the data table
224
- * @param {Boolean} cumulative - whether to make the data cumulative, or fill remove holes in the data. Defaults to true.
225
- * @returns
226
- */
227
- makeDataCumulative: (dataTable, cumulative = true) => {
228
- dataTable.rows.forEach((row, rowIndex) => {
229
- if (rowIndex > 0) {
230
- row.c.forEach((col, colIndex) => {
231
- if (colIndex > 0 && col.v == null) {
232
- col.v = Number(dataTable.rows[rowIndex - 1].c[colIndex].v)
233
- } else if (cumulative && colIndex > 0) {
234
- col.v = Number(col.v) + Number(dataTable.rows[rowIndex - 1].c[colIndex].v)
235
- }
236
- })
237
- }
238
- })
239
- return dataTable
240
- },
241
- getRactiveTableDataFormat: (rows, fields, formatting, styleTemplate = {}) => {
242
- const columns = Object.keys(fields).map(field => {
243
- // right align currency values
244
- if (formatting?.[field]?.type === 'NumberFormat' && formatting?.[field].format.prefix === '$') {
245
- return {
246
- property: `${field}[value]`,
247
- name: field,
248
- class: 'text-right border-top-0',
249
- }
250
- }
251
- return {
252
- property: `${field}[value]`,
253
- name: field,
254
- class: 'border-top-0',
255
- }
256
- })
257
- if (styleTemplate) {
258
- rows = rows.map(row => {
259
- return styleTableRow(row, styleTemplate)
260
- })
261
- }
262
- return {
263
- columns,
264
- rows,
265
- sortedRows: [],
266
- }
267
- },
268
- }
@@ -1,65 +0,0 @@
1
- /*
2
- SQLyog Ultimate v13.1.7 (64 bit)
3
- MySQL - 5.7.15-log : Database - itrackpro
4
- *********************************************************************
5
- */
6
-
7
- /*!40101 SET NAMES utf8 */;
8
-
9
- /*!40101 SET SQL_MODE=''*/;
10
-
11
- /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
12
- /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
13
- /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
14
- /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
15
- /*Table structure for table `dashboard_chart` */
16
-
17
- DROP TABLE IF EXISTS `dashboard_chart`;
18
-
19
- CREATE TABLE `dashboard_chart` (
20
- `chart_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
21
- `name` VARCHAR(64) NOT NULL COMMENT 'Internal name.',
22
- `title` VARCHAR(64) NOT NULL COMMENT 'Title shown to user.',
23
- `supertype` ENUM('google','table','embed') NOT NULL DEFAULT 'google' COMMENT 'Designates how the chart needs to be loaded and displayed.',
24
- `json` TEXT NOT NULL COMMENT 'JSON options for charts.',
25
- `show_chart` BIT(1) NOT NULL DEFAULT b'1' COMMENT 'Whether to show the chart in the charts list in the configuration screen.',
26
- PRIMARY KEY (`chart_id`)
27
- ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
28
-
29
- /*Table structure for table `dashboard_report` */
30
-
31
- DROP TABLE IF EXISTS `dashboard_report`;
32
-
33
- CREATE TABLE `dashboard_report` (
34
- `dashboard_report_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
35
- `report_name` VARCHAR(64) NOT NULL COMMENT 'Used internally in the program. Snake case.',
36
- `report_title` VARCHAR(64) NOT NULL COMMENT 'Shown to the user.',
37
- `json` TEXT NOT NULL COMMENT 'Parameters for the report. Array of objects.',
38
- `share_type` ENUM('user','group','store','everyone') NOT NULL DEFAULT 'everyone' COMMENT 'The type of id to use for sharing',
39
- `share_id` INT(11) DEFAULT NULL COMMENT 'User/Group/Store Id',
40
- `owner_id` INT(10) DEFAULT NULL COMMENT 'ID of the user who created the report. Used to control edit permissions on the client.',
41
- PRIMARY KEY (`dashboard_report_id`)
42
- ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
43
-
44
- /*Table structure for table `dashboard_report_chart` */
45
-
46
- DROP TABLE IF EXISTS `dashboard_report_chart`;
47
-
48
- CREATE TABLE `dashboard_report_chart` (
49
- `report_chart_id` INT(11) NOT NULL AUTO_INCREMENT,
50
- `dashboard_report_id` INT(10) UNSIGNED NOT NULL,
51
- `chart_id` INT(10) UNSIGNED NOT NULL,
52
- `rank` TINYINT(3) UNSIGNED NOT NULL DEFAULT '1' COMMENT 'Order to display this chart in this report.',
53
- `json_override` TEXT COMMENT 'Json - report-specific overrides for chart options.',
54
- PRIMARY KEY (`report_chart_id`),
55
- UNIQUE KEY `UC_Report_Chart` (`dashboard_report_id`,`chart_id`),
56
- KEY `maptochart` (`chart_id`),
57
- KEY `reporttomap` (`dashboard_report_id`),
58
- CONSTRAINT `maptochart` FOREIGN KEY (`chart_id`) REFERENCES `dashboard_chart` (`chart_id`) ON DELETE CASCADE ON UPDATE CASCADE,
59
- CONSTRAINT `reporttomap` FOREIGN KEY (`dashboard_report_id`) REFERENCES `dashboard_report` (`dashboard_report_id`) ON DELETE CASCADE ON UPDATE CASCADE
60
- ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
61
-
62
- /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
63
- /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
64
- /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
65
- /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
@@ -1,15 +0,0 @@
1
- {
2
- "formatterTemplates": {
3
- "CURRENCY": {
4
- "prefix": "$",
5
- "fractionDigits": 2,
6
- "groupingSymbol": ","
7
- },
8
- "MEDIUMDATE": {
9
- "format": "medium"
10
- },
11
- "LONGDATE": {
12
- "format": "long"
13
- }
14
- }
15
- }
package/index.js DELETED
@@ -1,284 +0,0 @@
1
- const db = require('@isoftdata/utility-db')
2
- const {
3
- getMultiSeriesData,
4
- makeNegativeNumbericValuesZero,
5
- coerceNullColumn,
6
- handleFormattingTemplates,
7
- getDataTableFormat,
8
- queryWithFields,
9
- makeDataCumulative,
10
- getRactiveTableDataFormat: getRactiveTableDataFormat } = require('./chart-helper.js')
11
- const { datesFromRange } = require('@isoftdata/utility-date-time')
12
- const { formatterTemplates } = require('./formatter-templates.json')
13
- const formatDate = require('date-fns/format')
14
- const camelcase = require('camelcase')
15
- const mysql = require('mysql')
16
- const deepmerge = require('deepmerge')
17
- const handleChartTypeSpecificQuirks = (chartType, resultSet) => {
18
- if (resultSet.length) {
19
- switch (chartType) {
20
- case 'PieChart':
21
- //PieCharts error if you have negative numbers, so we'll just set those to 0
22
- resultSet = makeNegativeNumbericValuesZero(resultSet)
23
- break
24
- case 'TreeMap':
25
- //tree charts need the 2nd columns to be not null
26
- resultSet = coerceNullColumn(Object.keys(resultSet[0])[1], resultSet)
27
- if (resultSet.length === 1) {
28
- resultSet[0].id = 'No Data'
29
- }
30
- break
31
- default:
32
- //all charts need the first column to be not null
33
- resultSet = coerceNullColumn(Object.keys(resultSet[0])[0], resultSet)
34
- }
35
- }
36
-
37
- return resultSet
38
- }
39
-
40
- const loadChartData = async(mysqlConnection, { query: queryObject, formatting, chartWrapper, multiSeries, table, supertype, cumulative, ...theRest }) => { // needs database connection
41
- if (!chartWrapper) {
42
- chartWrapper = {
43
- chartType: supertype,
44
- options: {
45
- height: 450, width: 800,
46
- },
47
- }
48
- }
49
-
50
- // There are area charts in the wild that need to be cumulative but aren't specifically flagged as such,
51
- // So make area charts cumulative unless they're explicitly set not to be
52
- cumulative = cumulative || (chartWrapper?.chartType === 'AreaChart' && cumulative !== false)
53
-
54
- let dataTable = {}, processedFormatting = {}
55
- try {
56
- const dataTableAndProcessedFormatting = await loadDataTableAndProcessedFormatting(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType: chartWrapper?.chartType, cumulative, table })
57
- dataTable = dataTableAndProcessedFormatting.dataTable
58
- processedFormatting = dataTableAndProcessedFormatting.processedFormatting
59
- } catch (err) {
60
- console.error('Error loading dataTable / processedFormatting')
61
- throw err
62
- }
63
-
64
- if (chartWrapper?.chartType === 'Table') {
65
- return {
66
- ...theRest,
67
- supertype,
68
- chartWrapper,
69
- table: { ...table, ...dataTable }, // combine the data and any table setting overrides
70
- formatting: processedFormatting,
71
- }
72
- }
73
- return {
74
- ...theRest,
75
- cumulative,
76
- supertype,
77
- chartWrapper: {
78
- ...chartWrapper,
79
- dataTable,
80
- },
81
- formatting: processedFormatting,
82
- }
83
- }
84
- /*
85
- * Returns only the data needed to render out the chart. If chartType is 'Table', it will return it in the format consumed by the ractive table component.
86
- * Otherwise, it will return data in the google chart Datatable object literal format.
87
- * multiSeries, formatting properties should be deconstructed from the chart object, chartType should be deconstructed from the chart's chartWrapper property
88
- */
89
- const loadDataTableAndProcessedFormatting = async(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType, cumulative, table }) => {
90
- let { results: queryResultSet, fields } = (queryObject && queryObject.sql) ? await queryWithFields(mysqlConnection, queryObject) : { fields: [], results: [] }
91
- let data = handleChartTypeSpecificQuirks(chartType, queryResultSet)
92
-
93
- if (multiSeries) {
94
- ({ data, formatting, fields } = getMultiSeriesData({
95
- data,
96
- seriesOptions: multiSeries,
97
- formatting,
98
- fields,
99
- }))
100
- }
101
-
102
- const processedFormatting = handleFormattingTemplates(formatting)
103
-
104
- if (chartType?.toLowerCase() === 'table') {
105
- return { dataTable: getRactiveTableDataFormat(data, fields, processedFormatting, table?.style), processedFormatting }
106
- }
107
-
108
- let dataTable = getDataTableFormat(data, fields, multiSeries) //per the Google Charts docs, the DataTable object literal format is the most performant, so we'll turn it into that format before returning
109
-
110
- // Stacked area charts will have holes / jagged lines if there are nulls in the middle of a column,
111
- // so always do this for those, even if we don't need it to be cumulative.
112
- if (cumulative || chartType === 'AreaChart') {
113
- dataTable = makeDataCumulative(dataTable, cumulative)
114
- }
115
-
116
- return { dataTable, processedFormatting }
117
- }
118
-
119
- const loadDataTable = async(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType }) => {
120
- const { dataTable } = await loadDataTableAndProcessedFormatting(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType })
121
- return dataTable
122
- }
123
- const loadOptionList = async(mysqlConnection, optionListQuery, optionList = []) => {
124
- return [
125
- ...optionList,
126
- // ...(optionListQuery && await query(mysqlConnection, optionListQuery)) || [],
127
- ...(optionListQuery && await db.query(mysqlConnection, optionListQuery)) || [],
128
-
129
- ]
130
- }
131
-
132
- const loadOutputParameterValues = async(mysqlConnection, { definitionList = [], selectionList = [], forClient = false, session = {} }) => {
133
- let parameterValues = []
134
-
135
- // If definitionList is not an Array, there are no parameters, so just return []
136
- if (!(definitionList instanceof Array)) {
137
- return []
138
- }
139
-
140
- for (const definition of definitionList) {
141
- const matchingParameterSelection = selectionList.find(param => param.name === definition.name)
142
-
143
- let parameter = {
144
- name: definition.name,
145
- type: definition.type,
146
- title: definition.title || definition.name,
147
- default: definition.default,
148
- value: (matchingParameterSelection && matchingParameterSelection.value) || definition.default,
149
- queryValues: {},
150
- }
151
-
152
- if (definition.type === 'dateRange') {
153
- parameter.dates = matchingParameterSelection?.dates || { to: null, from: null }
154
- parameter.value = parameter.value || 'Last 30 Days'
155
- if (parameter.value !== 'Custom') {
156
- parameter.dates = datesFromRange(parameter.value)
157
- }
158
- if (!forClient) {
159
- parameter.queryValues = {
160
- [`${parameter.name}_start_date`]: parameter.dates.from,
161
- [`${parameter.name}_end_date`]: parameter.dates.to,
162
- }
163
- }
164
- } else if (definition.type === 'selection') {
165
- parameter.optionList = await loadOptionList(mysqlConnection, definition.optionListQuery, definition.optionList) // queries db
166
- // Make sure the value they gave us is in the option list, otherwise take the first one, to prevent putting invalid values in the query
167
- if (!parameter.optionList.map(option => option.id).includes(parameter.value)) {
168
- console.log('Invalid parameter value supplied', parameter.value)
169
- parameter.value = parameter.optionList?.[0]?.id
170
- }
171
-
172
- const option = parameter.optionList.find(option => option.id === parameter.value)
173
-
174
- if (!forClient) {
175
- parameter.queryValues = {
176
- [parameter.name]: option.rawSql ? mysql.raw(parameter.value) : parameter.value,
177
- }
178
- }
179
- } else if (definition.type === 'date') {
180
- parameter.value = parameter.value || formatDate(new Date(), 'yyyy-MM-dd')
181
- if (!forClient) {
182
- parameter.queryValues = {
183
- [parameter.name]: parameter.value,
184
- }
185
- }
186
- } else if (definition.type === 'session') {
187
- // I think 'name' is the internal name of the parameter, and we should be able to expect that to be the name of the session variable
188
- // Or we could have a separate value for the name of the session variable
189
- // Might need to make this more complicated to handle nested variables
190
- parameter.value = session[camelcase(definition.name)]
191
- if (!forClient) {
192
- parameter.queryValues = {
193
- [parameter.name]: parameter.value,
194
- }
195
- }
196
- }
197
-
198
- parameterValues.push(parameter)
199
- }
200
-
201
- return parameterValues
202
- }
203
-
204
- /**
205
- *
206
- * @param {string} query - unparameterized query, with ${placeholders} for parameters
207
- * @param {Array<Object>} parameterValues - Parameter values to be plugged into the query
208
- * @returns {{sql: string, values: Array}}
209
- */
210
- const parameterizeQuery = (query, parameterValues) => {
211
- // Don't parameterize a lack of a query
212
- if (!query) {
213
- return {
214
- sql: '',
215
- values: [],
216
- }
217
- }
218
- const queryParameterValues = parameterValues.reduce((acc, val) => {
219
- return val.queryValues ? {
220
- ...acc,
221
- ...val.queryValues,
222
- } : acc
223
- }, {})
224
-
225
- //match on a token that starts with ${ and ends with a closing }.
226
- //Then include a capture group to get the inner part of that to use as a key
227
- const regexp = /\${([^}]+)}/g
228
-
229
- const queryParameterKeys = [ ...query.matchAll(regexp) ].map(([ fullToken, insideToken ]) => insideToken.trim())
230
- //if they put a token in the query that isn't one of the defined parameters of the report, just return null?
231
- const queryParameters = queryParameterKeys.map(key => queryParameterValues[key] || null)
232
-
233
- return {
234
- sql: query.replace(regexp, '?'),
235
- values: queryParameters,
236
- }
237
- }
238
-
239
- module.exports = {
240
- formatReportMetadata: async(mysqlConnection, { report, chartsInReport, session = {} }) => {
241
- try {
242
- const parameterValues = await loadOutputParameterValues(mysqlConnection, { definitionList: report.parameters, forClient: true, session })
243
-
244
- return {
245
- dashboardReportId: report.dashboardReportId,
246
- name: report.reportName,
247
- title: report.reportTitle,
248
- charts: chartsInReport,
249
- parameterValues,
250
- ownerId: report.ownerId,
251
- autoRefreshInterval: report.autoRefreshInterval,
252
- }
253
- } catch (err) {
254
- console.error(err)
255
- throw err
256
- }
257
- },
258
- formatChartDataForReport: async(mysqlConnection, { reportParameters, chart, reportChart, parameterSelectionList, session = {} }) => {
259
- try {
260
- const parameterValues = await loadOutputParameterValues(mysqlConnection, { definitionList: reportParameters, selectionList: parameterSelectionList, session })
261
- if (reportChart?.jsonOverride?.chartWrapper?.dataTable) {
262
- const { dataTable, ...chartWrapper } = reportChart.jsonOverride.chartWrapper
263
- reportChart.jsonOverride.chartWrapper = chartWrapper
264
- }
265
- const combinedChart = deepmerge(chart, reportChart.jsonOverride ?? {})
266
- return await loadChartData(mysqlConnection, {
267
- ...combinedChart,
268
- rank: reportChart.rank,
269
- reportChartId: reportChart.reportChartId,
270
- query: parameterizeQuery(chart.query, parameterValues),
271
- })
272
- } catch (err) {
273
- console.error(err)
274
- throw err
275
- }
276
- },
277
- loadOutputParameterValues,
278
- loadDataTable,
279
- loadDataTableAndProcessedFormatting,
280
- parameterizeQuery,
281
- handleFormattingTemplates,
282
- formatterTemplates,
283
- // TODO: function that diffs the default chart options with the ones being saved, and returns the difference to be saved as overrides
284
- }