@isoftdata/utility-dashboard-backend 1.5.7 → 2.0.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 +3 -3
- package/chart-helper.js +11 -4
- package/index.js +31 -11
- package/package.json +6 -3
- package/schema.md +2 -2
- package/style-templates.js +31 -0
- package/stylingTest.jsonc +37 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
A backend utility for formatting and loading dashboard chart and report data to be usable by the [frontend dashboard component](https://github.com/ISoft-Data-Systems/ractive-component-web-dashboard).
|
|
3
3
|
|
|
4
4
|
## Prerequisites
|
|
5
|
-
|
|
5
|
+
> Version 3.Y.Z of the frontend component is required for use with version 2.Y.Z of the backend component.
|
|
6
6
|
* A MySQL database schema as outlined in [schema.md](schema.md)
|
|
7
7
|
* A backend that can fetches rows from the chart, report, and report chart tables from the database with their primary keys. Any json columns should be parsed as objects.
|
|
8
8
|
* A connection or pool of connections from the `mysql` NPM module for running the charts' queries
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
This function returns data in the format expected for the `loadReportMetadata` function on the dashboard client component.
|
|
16
16
|
|
|
17
|
-
#### Function signature
|
|
17
|
+
#### Function signature
|
|
18
18
|
|
|
19
19
|
```js
|
|
20
20
|
async formatReportMetadata(mysqlConnection, { report, chartsInReport })
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
#### Example Usage (From Pro Web `server/chart-loader.js`)
|
|
23
|
+
#### Example Usage (From Pro Web `server/chart-loader.js`)
|
|
24
24
|
|
|
25
25
|
```js
|
|
26
26
|
async function loadReportMetadata({ dashboardReportId, chartId }) {
|
package/chart-helper.js
CHANGED
|
@@ -2,6 +2,7 @@ const { formatterTemplates } = require('./formatter-templates.json')
|
|
|
2
2
|
const toSnakeCase = require('to-snake-case')
|
|
3
3
|
const mysqlDataTypes = require('mysql/lib/protocol/constants/types')
|
|
4
4
|
const klona = require('klona')
|
|
5
|
+
const styleTableRow = require('./style-templates.js')
|
|
5
6
|
|
|
6
7
|
const mysqlToGoogleChartsDataTypeMap = mysqlDataType => {
|
|
7
8
|
switch (mysqlDataType) {
|
|
@@ -52,6 +53,7 @@ module.exports = {
|
|
|
52
53
|
return new Promise((resolve, reject) => {
|
|
53
54
|
connection.query(query, (err, results, fields) => {
|
|
54
55
|
if (err) {
|
|
56
|
+
console.error('Error running query for dashboard chart', err)
|
|
55
57
|
reject(err)
|
|
56
58
|
} else {
|
|
57
59
|
resolve({
|
|
@@ -236,25 +238,30 @@ module.exports = {
|
|
|
236
238
|
})
|
|
237
239
|
return dataTable
|
|
238
240
|
},
|
|
239
|
-
getRactiveTableDataFormat: (
|
|
241
|
+
getRactiveTableDataFormat: (rows, fields, formatting, styleTemplate = {}) => {
|
|
240
242
|
const columns = Object.keys(fields).map(field => {
|
|
241
243
|
// right align currency values
|
|
242
244
|
if (formatting?.[field]?.type === 'NumberFormat' && formatting?.[field].format.prefix === '$') {
|
|
243
245
|
return {
|
|
244
|
-
property: field
|
|
246
|
+
property: `${field}.value`,
|
|
245
247
|
name: field,
|
|
246
248
|
class: 'text-right border-top-0',
|
|
247
249
|
}
|
|
248
250
|
}
|
|
249
251
|
return {
|
|
250
|
-
property: field
|
|
252
|
+
property: `${field}.value`,
|
|
251
253
|
name: field,
|
|
252
254
|
class: 'border-top-0',
|
|
253
255
|
}
|
|
254
256
|
})
|
|
257
|
+
if (styleTemplate) {
|
|
258
|
+
rows = rows.map(row => {
|
|
259
|
+
return styleTableRow(row, styleTemplate)
|
|
260
|
+
})
|
|
261
|
+
}
|
|
255
262
|
return {
|
|
256
263
|
columns,
|
|
257
|
-
rows
|
|
264
|
+
rows,
|
|
258
265
|
sortedRows: [],
|
|
259
266
|
}
|
|
260
267
|
},
|
package/index.js
CHANGED
|
@@ -12,7 +12,8 @@ const { datesFromRange } = require('@isoftdata/utility-date-time')
|
|
|
12
12
|
const { formatterTemplates } = require('./formatter-templates.json')
|
|
13
13
|
const formatDate = require('date-fns/format')
|
|
14
14
|
const camelcase = require('camelcase')
|
|
15
|
-
|
|
15
|
+
const mysql = require('mysql')
|
|
16
|
+
const deepmerge = require('deepmerge')
|
|
16
17
|
const handleChartTypeSpecificQuirks = (chartType, resultSet) => {
|
|
17
18
|
if (resultSet.length) {
|
|
18
19
|
switch (chartType) {
|
|
@@ -45,7 +46,20 @@ const loadChartData = async(mysqlConnection, { query: queryObject, formatting, c
|
|
|
45
46
|
},
|
|
46
47
|
}
|
|
47
48
|
}
|
|
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
|
+
}
|
|
49
63
|
|
|
50
64
|
if (chartWrapper?.chartType === 'Table') {
|
|
51
65
|
return {
|
|
@@ -58,6 +72,7 @@ const loadChartData = async(mysqlConnection, { query: queryObject, formatting, c
|
|
|
58
72
|
}
|
|
59
73
|
return {
|
|
60
74
|
...theRest,
|
|
75
|
+
cumulative,
|
|
61
76
|
supertype,
|
|
62
77
|
chartWrapper: {
|
|
63
78
|
...chartWrapper,
|
|
@@ -71,7 +86,7 @@ const loadChartData = async(mysqlConnection, { query: queryObject, formatting, c
|
|
|
71
86
|
* Otherwise, it will return data in the google chart Datatable object literal format.
|
|
72
87
|
* multiSeries, formatting properties should be deconstructed from the chart object, chartType should be deconstructed from the chart's chartWrapper property
|
|
73
88
|
*/
|
|
74
|
-
const loadDataTableAndProcessedFormatting = async(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType, cumulative }) => {
|
|
89
|
+
const loadDataTableAndProcessedFormatting = async(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType, cumulative, table }) => {
|
|
75
90
|
let { results: queryResultSet, fields } = (queryObject && queryObject.sql) ? await queryWithFields(mysqlConnection, queryObject) : { fields: [], results: [] }
|
|
76
91
|
let data = handleChartTypeSpecificQuirks(chartType, queryResultSet)
|
|
77
92
|
|
|
@@ -87,15 +102,11 @@ const loadDataTableAndProcessedFormatting = async(mysqlConnection, { query: quer
|
|
|
87
102
|
const processedFormatting = handleFormattingTemplates(formatting)
|
|
88
103
|
|
|
89
104
|
if (chartType?.toLowerCase() === 'table') {
|
|
90
|
-
return { dataTable: getRactiveTableDataFormat(data, fields, processedFormatting), processedFormatting }
|
|
105
|
+
return { dataTable: getRactiveTableDataFormat(data, fields, processedFormatting, table?.style), processedFormatting }
|
|
91
106
|
}
|
|
92
107
|
|
|
93
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
|
|
94
109
|
|
|
95
|
-
// There are area charts in the wild that need to be cumulative but aren't specifically flagged as such,
|
|
96
|
-
// So make area charts cumulative unless they're explicitly set not to be
|
|
97
|
-
cumulative = cumulative || (chartType === 'AreaChart' && cumulative !== false)
|
|
98
|
-
|
|
99
110
|
// Stacked area charts will have holes / jagged lines if there are nulls in the middle of a column,
|
|
100
111
|
// so always do this for those, even if we don't need it to be cumulative.
|
|
101
112
|
if (cumulative || chartType === 'AreaChart') {
|
|
@@ -152,9 +163,17 @@ const loadOutputParameterValues = async(mysqlConnection, { definitionList = [],
|
|
|
152
163
|
}
|
|
153
164
|
} else if (definition.type === 'selection') {
|
|
154
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
|
+
|
|
155
174
|
if (!forClient) {
|
|
156
175
|
parameter.queryValues = {
|
|
157
|
-
[parameter.name]: parameter.value,
|
|
176
|
+
[parameter.name]: option.rawSql ? mysql.raw(parameter.value) : parameter.value,
|
|
158
177
|
}
|
|
159
178
|
}
|
|
160
179
|
} else if (definition.type === 'date') {
|
|
@@ -243,11 +262,11 @@ module.exports = {
|
|
|
243
262
|
const { dataTable, ...chartWrapper } = reportChart.jsonOverride.chartWrapper
|
|
244
263
|
reportChart.jsonOverride.chartWrapper = chartWrapper
|
|
245
264
|
}
|
|
265
|
+
const combinedChart = deepmerge(chart, reportChart.jsonOverride ?? {})
|
|
246
266
|
return await loadChartData(mysqlConnection, {
|
|
247
|
-
...
|
|
267
|
+
...combinedChart,
|
|
248
268
|
rank: reportChart.rank,
|
|
249
269
|
reportChartId: reportChart.reportChartId,
|
|
250
|
-
...reportChart.jsonOverride, // overwrites whole chart wrapper :\
|
|
251
270
|
query: parameterizeQuery(chart.query, parameterValues),
|
|
252
271
|
})
|
|
253
272
|
} catch (err) {
|
|
@@ -261,4 +280,5 @@ module.exports = {
|
|
|
261
280
|
parameterizeQuery,
|
|
262
281
|
handleFormattingTemplates,
|
|
263
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
|
|
264
284
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isoftdata/utility-dashboard-backend",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A utility for formatting chart and report data to be usable by the frontend dashboard component.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@isoftdata/utility-date-time": "^2.0.3",
|
|
13
13
|
"@isoftdata/utility-db": "^1.4.0",
|
|
14
|
+
"camelcase": "^5.3.1",
|
|
14
15
|
"date-fns": "^2.23.0",
|
|
16
|
+
"deepmerge": "^4.2.2",
|
|
15
17
|
"klona": "^1.1.2",
|
|
18
|
+
"mathjs": "^11.5.0",
|
|
16
19
|
"mysql": "^2.18.1",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
20
|
+
"snakeize": "^0.1.0",
|
|
21
|
+
"to-snake-case": "^1.0.0"
|
|
19
22
|
}
|
|
20
23
|
}
|
package/schema.md
CHANGED
|
@@ -37,12 +37,12 @@ Property | Type | Description
|
|
|
37
37
|
default | `String` | Can be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date string(eg. `'2021-01-13'`). If the `default` property is omitted or [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), the today's date will be used.
|
|
38
38
|
|
|
39
39
|
##### `selection` Type Parameters
|
|
40
|
-
> In the UI, the user will see a dropdown with the `optionList` values(if any) and the values returned from the `optionListQuery`(if any).
|
|
40
|
+
> In the UI, the user will see a dropdown with the `optionList` values(if any) and the values returned from the `optionListQuery`(if any). If the user passes a value that isn't in the `optionList`, the first option in the array will be used instead.
|
|
41
41
|
|
|
42
42
|
Property | Type | Description
|
|
43
43
|
---- | ---- | -----------
|
|
44
44
|
default | `String` | The `id` of the option you'd like to be selected by default
|
|
45
|
-
optionList | `Object` | Each object should have an `id`, and `name`. `id` is the value that will be given to you at query time and `name` is what will be displayed in the dropdown to the user.
|
|
45
|
+
optionList | `Object` | Each object should have an `id`, and `name`. The `id` is the value that will be given to you at query time and `name` is what will be displayed in the dropdown to the user. Values are [escaped by the mysql library](https://github.com/mysqljs/mysql#escaping-query-values), unless `rawSql: true` is on an option's object.
|
|
46
46
|
optionListQuery | `String` | A query that selects 2 columns `id` and `name`. The results of this query will be merged with `optionList` and put in the dropdown for the user.
|
|
47
47
|
|
|
48
48
|
##### `session` Type Parameters
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
const mathjs = require('mathjs')
|
|
3
|
+
const snakeize = require('snakeize')
|
|
4
|
+
|
|
5
|
+
function handleMathJsCellStyleTemplate(row, columnToStyle, templates) {
|
|
6
|
+
let outColumn = { value: row[columnToStyle] }
|
|
7
|
+
if (columnToStyle in templates) {
|
|
8
|
+
// MathJS mutates the object you pass it, and we need to ensure keys are single "word" strings, so snakeize a copy of the row
|
|
9
|
+
const scope = snakeize({ ...row })
|
|
10
|
+
for (const key in templates[columnToStyle]) {
|
|
11
|
+
try {
|
|
12
|
+
outColumn[key] = mathjs.evaluate(templates[columnToStyle][key], scope)
|
|
13
|
+
} catch (err) {
|
|
14
|
+
// Obviously we don't want to break the whole table if one cell has a bad template
|
|
15
|
+
// For now, just log it to stderr and allow it to fall through even though it's not super debuggable
|
|
16
|
+
console.error(`Error evaluating expression: '${templates[columnToStyle][key]}'\n`, err)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return outColumn
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function styleRow(row, templates) {
|
|
24
|
+
const outRow = {}
|
|
25
|
+
for (const column in row) {
|
|
26
|
+
outRow[column] = handleMathJsCellStyleTemplate(row, column, templates)
|
|
27
|
+
}
|
|
28
|
+
return outRow
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = styleRow
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"query": "SELECT 'False' AS 'Seller Invoice', 'True' AS 'Rec. Payment', '' AS 'Paid For'",
|
|
3
|
+
"formatting": {},
|
|
4
|
+
"chartWrapper": {
|
|
5
|
+
"options": {
|
|
6
|
+
"width": 500,
|
|
7
|
+
"height": 500,
|
|
8
|
+
"width_units": "%"
|
|
9
|
+
},
|
|
10
|
+
"chartType": "Table"
|
|
11
|
+
},
|
|
12
|
+
"greedyWidth": true,
|
|
13
|
+
"greedyHeight": true,
|
|
14
|
+
"hideTitle": true,
|
|
15
|
+
"table": {
|
|
16
|
+
"perPageCount": 15,
|
|
17
|
+
"showFooter": true,
|
|
18
|
+
"allowExport": true,
|
|
19
|
+
"style": {
|
|
20
|
+
"Seller Invoice": { // Column to style
|
|
21
|
+
"columnToCheck": false, // column to check condition against, falsy = this column
|
|
22
|
+
"in": [ // value is in the set
|
|
23
|
+
"False",
|
|
24
|
+
""
|
|
25
|
+
],
|
|
26
|
+
"class": "table-danger" // class to apply
|
|
27
|
+
},
|
|
28
|
+
"Rec. Payment": { // Column to style
|
|
29
|
+
"columnToCheck": "Paid For", // column to check condition against
|
|
30
|
+
"notIn": [ // value is not in the set
|
|
31
|
+
""
|
|
32
|
+
],
|
|
33
|
+
"class": "table-danger" // class to apply
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|