@isoftdata/utility-dashboard-backend 2.0.1 → 2.1.0
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/README.md +19 -6
- package/dist/chart-helper.d.ts +32 -0
- package/dist/chart-helper.js +240 -0
- package/dist/formatting-templates.d.ts +3 -0
- package/dist/formatting-templates.js +17 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +321 -0
- package/dist/style-templates.d.ts +4 -0
- package/dist/style-templates.js +33 -0
- package/dist/types/chart.d.ts +91 -0
- package/dist/types/chart.js +3 -0
- package/dist/types/parameter.d.ts +37 -0
- package/dist/types/parameter.js +3 -0
- package/dist/types/report.d.ts +24 -0
- package/dist/types/report.js +3 -0
- package/dist/types/session.d.ts +9 -0
- package/dist/types/session.js +3 -0
- package/package.json +26 -5
- package/.gitattributes +0 -2
- package/chart-helper.js +0 -268
- package/empty dashboard schema.sql +0 -65
- package/formatter-templates.json +0 -15
- package/index.js +0 -284
- package/schema.md +0 -125
- package/style-templates.js +0 -31
- package/stylingTest.jsonc +0 -37
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 */;
|
package/formatter-templates.json
DELETED
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
|
-
}
|