@isoftdata/utility-dashboard-backend 1.5.8 → 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 +25 -7
- 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) {
|
|
@@ -50,7 +51,15 @@ const loadChartData = async(mysqlConnection, { query: queryObject, formatting, c
|
|
|
50
51
|
// So make area charts cumulative unless they're explicitly set not to be
|
|
51
52
|
cumulative = cumulative || (chartWrapper?.chartType === 'AreaChart' && cumulative !== false)
|
|
52
53
|
|
|
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
|
+
}
|
|
54
63
|
|
|
55
64
|
if (chartWrapper?.chartType === 'Table') {
|
|
56
65
|
return {
|
|
@@ -77,7 +86,7 @@ const loadChartData = async(mysqlConnection, { query: queryObject, formatting, c
|
|
|
77
86
|
* Otherwise, it will return data in the google chart Datatable object literal format.
|
|
78
87
|
* multiSeries, formatting properties should be deconstructed from the chart object, chartType should be deconstructed from the chart's chartWrapper property
|
|
79
88
|
*/
|
|
80
|
-
const loadDataTableAndProcessedFormatting = async(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType, cumulative }) => {
|
|
89
|
+
const loadDataTableAndProcessedFormatting = async(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType, cumulative, table }) => {
|
|
81
90
|
let { results: queryResultSet, fields } = (queryObject && queryObject.sql) ? await queryWithFields(mysqlConnection, queryObject) : { fields: [], results: [] }
|
|
82
91
|
let data = handleChartTypeSpecificQuirks(chartType, queryResultSet)
|
|
83
92
|
|
|
@@ -93,7 +102,7 @@ const loadDataTableAndProcessedFormatting = async(mysqlConnection, { query: quer
|
|
|
93
102
|
const processedFormatting = handleFormattingTemplates(formatting)
|
|
94
103
|
|
|
95
104
|
if (chartType?.toLowerCase() === 'table') {
|
|
96
|
-
return { dataTable: getRactiveTableDataFormat(data, fields, processedFormatting), processedFormatting }
|
|
105
|
+
return { dataTable: getRactiveTableDataFormat(data, fields, processedFormatting, table?.style), processedFormatting }
|
|
97
106
|
}
|
|
98
107
|
|
|
99
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
|
|
@@ -154,9 +163,17 @@ const loadOutputParameterValues = async(mysqlConnection, { definitionList = [],
|
|
|
154
163
|
}
|
|
155
164
|
} else if (definition.type === 'selection') {
|
|
156
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
|
+
|
|
157
174
|
if (!forClient) {
|
|
158
175
|
parameter.queryValues = {
|
|
159
|
-
[parameter.name]: parameter.value,
|
|
176
|
+
[parameter.name]: option.rawSql ? mysql.raw(parameter.value) : parameter.value,
|
|
160
177
|
}
|
|
161
178
|
}
|
|
162
179
|
} else if (definition.type === 'date') {
|
|
@@ -245,11 +262,11 @@ module.exports = {
|
|
|
245
262
|
const { dataTable, ...chartWrapper } = reportChart.jsonOverride.chartWrapper
|
|
246
263
|
reportChart.jsonOverride.chartWrapper = chartWrapper
|
|
247
264
|
}
|
|
265
|
+
const combinedChart = deepmerge(chart, reportChart.jsonOverride ?? {})
|
|
248
266
|
return await loadChartData(mysqlConnection, {
|
|
249
|
-
...
|
|
267
|
+
...combinedChart,
|
|
250
268
|
rank: reportChart.rank,
|
|
251
269
|
reportChartId: reportChart.reportChartId,
|
|
252
|
-
...reportChart.jsonOverride, // overwrites whole chart wrapper :\
|
|
253
270
|
query: parameterizeQuery(chart.query, parameterValues),
|
|
254
271
|
})
|
|
255
272
|
} catch (err) {
|
|
@@ -263,4 +280,5 @@ module.exports = {
|
|
|
263
280
|
parameterizeQuery,
|
|
264
281
|
handleFormattingTemplates,
|
|
265
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
|
|
266
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
|
+
}
|