@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 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: (data, fields, formatting) => {
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: data,
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
- const { dataTable, processedFormatting } = await loadDataTableAndProcessedFormatting(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType: chartWrapper?.chartType, cumulative })
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
- ...chart,
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": "1.5.8",
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
- "to-snake-case": "^1.0.0",
18
- "camelcase": "^5.3.1"
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
+ }