@isoftdata/utility-dashboard-backend 1.3.0 → 1.4.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/index.js CHANGED
@@ -1,208 +1,208 @@
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 formatDate = require('date-fns/format')
13
-
14
- const handleChartTypeSpecificQuirks = (chartType, resultSet) => {
15
- if (resultSet.length) {
16
- switch (chartType) {
17
- case 'PieChart':
18
- //PieCharts error if you have negative numbers, so we'll just set those to 0
19
- resultSet = makeNegativeNumbericValuesZero(resultSet)
20
- break
21
- case 'TreeMap':
22
- //tree charts need the 2nd columns to be not null
23
- resultSet = coerceNullColumn(Object.keys(resultSet[0])[1], resultSet)
24
- break
25
- default:
26
- //all charts need the first column to be not null
27
- resultSet = coerceNullColumn(Object.keys(resultSet[0])[0], resultSet)
28
- }
29
- }
30
-
31
- return resultSet
32
- }
33
-
34
- const loadChartData = async (mysqlConnection, { name, query: queryObject, formatting, chartWrapper, multiSeries, parameters, ...theRest }) => { // needs database connection
35
- const dataTable = await loadDataTable(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType: chartWrapper.chartType })
36
-
37
- const proccessedFormatting = handleFormattingTemplates(formatting)
38
-
39
- if (chartWrapper.chartType === 'Table') {
40
- return {
41
- ...theRest,
42
- chartWrapper,
43
- table: dataTable,
44
- formatting: proccessedFormatting,
45
- }
46
- }
47
- return {
48
- ...theRest,
49
- chartWrapper: {
50
- ...chartWrapper,
51
- dataTable,
52
- },
53
- formatting: proccessedFormatting,
54
- }
55
- }
56
- /*
57
- * 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.
58
- * Otherwise, it will return data in the google chart Datatable object literal format.
59
- * multiSeries, formatting properties should be deconstructed from the chart object, chartType should be deconstructed from the chart's chartWrapper property
60
- */
61
- const loadDataTable = async (mysqlConnection, { query: queryObject, multiSeries, formatting, chartType }) => {
62
- let { results: queryResultSet, fields } = (queryObject && queryObject.sql) ? await queryWithFields(mysqlConnection, queryObject) : { fields: [], results: [] }
63
- let data = handleChartTypeSpecificQuirks(chartType, queryResultSet)
64
-
65
- if (chartType.toLowerCase() === 'table') {
66
- return getRactiveTableDataFormat(data, fields, formatting)
67
- }
68
-
69
- if (multiSeries) {
70
- ({ data, formatting, fields } = getMultiSeriesData({
71
- data,
72
- seriesOptions: multiSeries,
73
- formatting,
74
- fields,
75
- }))
76
- }
77
-
78
- 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
79
-
80
- // Stacked area charts will have holes / jagged lines if there are nulls in the middle of a column
81
- // Presumably we'll also want to be able to have non-cumulative area charts at some point
82
- if (chartType === 'AreaChart') {
83
- dataTable = makeDataCumulative(dataTable)
84
- }
85
-
86
- return dataTable
87
- }
88
-
89
- const loadOptionList = async (mysqlConnection, optionListQuery, optionList = []) => {
90
- return [
91
- ...optionList,
92
- // ...(optionListQuery && await query(mysqlConnection, optionListQuery)) || [],
93
- ...(optionListQuery && await db.query(mysqlConnection, optionListQuery)) || [],
94
-
95
- ]
96
- }
97
-
98
- const loadOutputParameterValues = async (mysqlConnection, { definitionList, selectionList = [], forClient = false }) => {
99
- let parameterValues = []
100
-
101
- for (const definition of definitionList) {
102
- const matchingParameterSelection = selectionList.find(param => param.name === definition.name)
103
-
104
- let parameter = {
105
- name: definition.name,
106
- type: definition.type,
107
- title: definition.title || definition.name,
108
- default: definition.default,
109
- value: (matchingParameterSelection && matchingParameterSelection.value) || definition.default,
110
- queryValues: {},
111
- }
112
-
113
- if (definition.type === 'dateRange') {
114
- parameter.dates = matchingParameterSelection?.dates || { to: null, from: null }
115
- parameter.value = parameter.value || 'Last 30 Days'
116
- if (parameter.value !== 'Custom') {
117
- parameter.dates = datesFromRange(parameter.value)
118
- }
119
- if (!forClient) {
120
- parameter.queryValues = {
121
- [`${parameter.name}_start_date`]: parameter.dates.from,
122
- [`${parameter.name}_end_date`]: parameter.dates.to,
123
- }
124
- }
125
- } else if (definition.type === 'selection') {
126
- parameter.optionList = await loadOptionList(mysqlConnection, definition.optionListQuery, definition.optionList) // queries db
127
- if (!forClient) {
128
- parameter.queryValues = {
129
- [parameter.name]: parameter.value,
130
- }
131
- }
132
- } else if (definition.type === 'date') {
133
- parameter.value = parameter.value || formatDate(new Date(), 'yyyy-MM-dd')
134
- if (!forClient) {
135
- parameter.queryValues = {
136
- [parameter.name]: parameter.value,
137
- }
138
- }
139
- }
140
-
141
- parameterValues.push(parameter)
142
- }
143
-
144
- return parameterValues
145
- }
146
-
147
- const parameterizeQuery = (query, parameterValues) => {
148
- const queryParameterValues = parameterValues.reduce((acc, val) => {
149
- return val.queryValues ? {
150
- ...acc,
151
- ...val.queryValues,
152
- } : acc
153
- }, {})
154
-
155
- //match on a token that starts with ${ and ends with a closing }.
156
- //Then include a capture group to get the inner part of that to use as a key
157
- const regexp = /\${([^}]+)}/g
158
-
159
- const queryParameterKeys = [...query.matchAll(regexp)].map(([fullToken, insideToken]) => insideToken.trim())
160
- //if they put a token in the query that isn't one of the defined parameters of the report, just return null?
161
- const queryParameters = queryParameterKeys.map(key => queryParameterValues[key] || null)
162
-
163
- return {
164
- sql: query.replace(regexp, '?'),
165
- values: queryParameters,
166
- }
167
- }
168
-
169
- module.exports = {
170
- formatReportMetadata: async (mysqlConnection, { report, chartsInReport }) => {
171
- try {
172
- const parameterValues = await loadOutputParameterValues(mysqlConnection, { definitionList: report.parameters, forClient: true })
173
-
174
- return {
175
- dashboardReportId: report.dashboardReportId,
176
- name: report.reportName,
177
- title: report.reportTitle,
178
- charts: chartsInReport,
179
- parameterValues,
180
- ownerId: report.ownerId,
181
- }
182
- } catch (err) {
183
- console.error(err)
184
- throw err
185
- }
186
- },
187
- formatChartDataForReport: async (mysqlConnection, { reportParameters, chart, reportChart, parameterSelectionList }) => {
188
- try {
189
- const parameterValues = await loadOutputParameterValues(mysqlConnection, { definitionList: reportParameters, selectionList: parameterSelectionList })
190
- if (reportChart?.jsonOverride?.chartWrapper?.dataTable) {
191
- const { dataTable, ...chartWrapper } = reportChart.jsonOverride.chartWrapper
192
- reportChart.jsonOverride.chartWrapper = chartWrapper
193
- }
194
- return await loadChartData(mysqlConnection, {
195
- ...chart,
196
- reportChartId: reportChart.reportChartId,
197
- ...reportChart.jsonOverride, // overwrites whole chart wrapper :\
198
- query: parameterizeQuery(chart.query, parameterValues),
199
- })
200
- } catch (err) {
201
- console.error(err)
202
- throw err
203
- }
204
- },
205
- loadOutputParameterValues,
206
- loadDataTable,
207
- parameterizeQuery,
208
- }
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 formatDate = require('date-fns/format')
13
+
14
+ const handleChartTypeSpecificQuirks = (chartType, resultSet) => {
15
+ if (resultSet.length) {
16
+ switch (chartType) {
17
+ case 'PieChart':
18
+ //PieCharts error if you have negative numbers, so we'll just set those to 0
19
+ resultSet = makeNegativeNumbericValuesZero(resultSet)
20
+ break
21
+ case 'TreeMap':
22
+ //tree charts need the 2nd columns to be not null
23
+ resultSet = coerceNullColumn(Object.keys(resultSet[0])[1], resultSet)
24
+ break
25
+ default:
26
+ //all charts need the first column to be not null
27
+ resultSet = coerceNullColumn(Object.keys(resultSet[0])[0], resultSet)
28
+ }
29
+ }
30
+
31
+ return resultSet
32
+ }
33
+
34
+ const loadChartData = async (mysqlConnection, { name, query: queryObject, formatting, chartWrapper, multiSeries, parameters, ...theRest }) => { // needs database connection
35
+ const dataTable = await loadDataTable(mysqlConnection, { query: queryObject, multiSeries, formatting, chartType: chartWrapper.chartType })
36
+
37
+ const proccessedFormatting = handleFormattingTemplates(formatting)
38
+
39
+ if (chartWrapper.chartType === 'Table') {
40
+ return {
41
+ ...theRest,
42
+ chartWrapper,
43
+ table: dataTable,
44
+ formatting: proccessedFormatting,
45
+ }
46
+ }
47
+ return {
48
+ ...theRest,
49
+ chartWrapper: {
50
+ ...chartWrapper,
51
+ dataTable,
52
+ },
53
+ formatting: proccessedFormatting,
54
+ }
55
+ }
56
+ /*
57
+ * 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.
58
+ * Otherwise, it will return data in the google chart Datatable object literal format.
59
+ * multiSeries, formatting properties should be deconstructed from the chart object, chartType should be deconstructed from the chart's chartWrapper property
60
+ */
61
+ const loadDataTable = async (mysqlConnection, { query: queryObject, multiSeries, formatting, chartType }) => {
62
+ let { results: queryResultSet, fields } = (queryObject && queryObject.sql) ? await queryWithFields(mysqlConnection, queryObject) : { fields: [], results: [] }
63
+ let data = handleChartTypeSpecificQuirks(chartType, queryResultSet)
64
+
65
+ if (chartType.toLowerCase() === 'table') {
66
+ return getRactiveTableDataFormat(data, fields, formatting)
67
+ }
68
+
69
+ if (multiSeries) {
70
+ ({ data, formatting, fields } = getMultiSeriesData({
71
+ data,
72
+ seriesOptions: multiSeries,
73
+ formatting,
74
+ fields,
75
+ }))
76
+ }
77
+
78
+ 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
79
+
80
+ // Stacked area charts will have holes / jagged lines if there are nulls in the middle of a column
81
+ // Presumably we'll also want to be able to have non-cumulative area charts at some point
82
+ if (chartType === 'AreaChart') {
83
+ dataTable = makeDataCumulative(dataTable)
84
+ }
85
+
86
+ return dataTable
87
+ }
88
+
89
+ const loadOptionList = async (mysqlConnection, optionListQuery, optionList = []) => {
90
+ return [
91
+ ...optionList,
92
+ // ...(optionListQuery && await query(mysqlConnection, optionListQuery)) || [],
93
+ ...(optionListQuery && await db.query(mysqlConnection, optionListQuery)) || [],
94
+
95
+ ]
96
+ }
97
+
98
+ const loadOutputParameterValues = async (mysqlConnection, { definitionList, selectionList = [], forClient = false }) => {
99
+ let parameterValues = []
100
+
101
+ for (const definition of definitionList) {
102
+ const matchingParameterSelection = selectionList.find(param => param.name === definition.name)
103
+
104
+ let parameter = {
105
+ name: definition.name,
106
+ type: definition.type,
107
+ title: definition.title || definition.name,
108
+ default: definition.default,
109
+ value: (matchingParameterSelection && matchingParameterSelection.value) || definition.default,
110
+ queryValues: {},
111
+ }
112
+
113
+ if (definition.type === 'dateRange') {
114
+ parameter.dates = matchingParameterSelection?.dates || { to: null, from: null }
115
+ parameter.value = parameter.value || 'Last 30 Days'
116
+ if (parameter.value !== 'Custom') {
117
+ parameter.dates = datesFromRange(parameter.value)
118
+ }
119
+ if (!forClient) {
120
+ parameter.queryValues = {
121
+ [`${parameter.name}_start_date`]: parameter.dates.from,
122
+ [`${parameter.name}_end_date`]: parameter.dates.to,
123
+ }
124
+ }
125
+ } else if (definition.type === 'selection') {
126
+ parameter.optionList = await loadOptionList(mysqlConnection, definition.optionListQuery, definition.optionList) // queries db
127
+ if (!forClient) {
128
+ parameter.queryValues = {
129
+ [parameter.name]: parameter.value,
130
+ }
131
+ }
132
+ } else if (definition.type === 'date') {
133
+ parameter.value = parameter.value || formatDate(new Date(), 'yyyy-MM-dd')
134
+ if (!forClient) {
135
+ parameter.queryValues = {
136
+ [parameter.name]: parameter.value,
137
+ }
138
+ }
139
+ }
140
+
141
+ parameterValues.push(parameter)
142
+ }
143
+
144
+ return parameterValues
145
+ }
146
+
147
+ const parameterizeQuery = (query, parameterValues) => {
148
+ const queryParameterValues = parameterValues.reduce((acc, val) => {
149
+ return val.queryValues ? {
150
+ ...acc,
151
+ ...val.queryValues,
152
+ } : acc
153
+ }, {})
154
+
155
+ //match on a token that starts with ${ and ends with a closing }.
156
+ //Then include a capture group to get the inner part of that to use as a key
157
+ const regexp = /\${([^}]+)}/g
158
+
159
+ const queryParameterKeys = [...query.matchAll(regexp)].map(([fullToken, insideToken]) => insideToken.trim())
160
+ //if they put a token in the query that isn't one of the defined parameters of the report, just return null?
161
+ const queryParameters = queryParameterKeys.map(key => queryParameterValues[key] || null)
162
+
163
+ return {
164
+ sql: query.replace(regexp, '?'),
165
+ values: queryParameters,
166
+ }
167
+ }
168
+
169
+ module.exports = {
170
+ formatReportMetadata: async (mysqlConnection, { report, chartsInReport }) => {
171
+ try {
172
+ const parameterValues = await loadOutputParameterValues(mysqlConnection, { definitionList: report.parameters, forClient: true })
173
+
174
+ return {
175
+ dashboardReportId: report.dashboardReportId,
176
+ name: report.reportName,
177
+ title: report.reportTitle,
178
+ charts: chartsInReport,
179
+ parameterValues,
180
+ ownerId: report.ownerId,
181
+ }
182
+ } catch (err) {
183
+ console.error(err)
184
+ throw err
185
+ }
186
+ },
187
+ formatChartDataForReport: async (mysqlConnection, { reportParameters, chart, reportChart, parameterSelectionList }) => {
188
+ try {
189
+ const parameterValues = await loadOutputParameterValues(mysqlConnection, { definitionList: reportParameters, selectionList: parameterSelectionList })
190
+ if (reportChart?.jsonOverride?.chartWrapper?.dataTable) {
191
+ const { dataTable, ...chartWrapper } = reportChart.jsonOverride.chartWrapper
192
+ reportChart.jsonOverride.chartWrapper = chartWrapper
193
+ }
194
+ return await loadChartData(mysqlConnection, {
195
+ ...chart,
196
+ reportChartId: reportChart.reportChartId,
197
+ ...reportChart.jsonOverride, // overwrites whole chart wrapper :\
198
+ query: parameterizeQuery(chart.query, parameterValues),
199
+ })
200
+ } catch (err) {
201
+ console.error(err)
202
+ throw err
203
+ }
204
+ },
205
+ loadOutputParameterValues,
206
+ loadDataTable,
207
+ parameterizeQuery,
208
+ }
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
- {
2
- "name": "@isoftdata/utility-dashboard-backend",
3
- "version": "1.3.0",
4
- "description": "A utility for formatting chart and report data to be usable by the frontend dashboard component.",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "author": "Charles Kaup",
10
- "license": "ISC",
11
- "dependencies": {
12
- "@isoftdata/utility-date-time": "^2.0.3",
13
- "@isoftdata/utility-db": "^1.4.0",
14
- "date-fns": "^2.23.0",
15
- "klona": "^1.1.2",
16
- "mysql": "^2.18.1",
17
- "to-snake-case": "^1.0.0"
18
- }
19
- }
1
+ {
2
+ "name": "@isoftdata/utility-dashboard-backend",
3
+ "version": "1.4.1",
4
+ "description": "A utility for formatting chart and report data to be usable by the frontend dashboard component.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "Charles Kaup",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "@isoftdata/utility-date-time": "^2.0.3",
13
+ "@isoftdata/utility-db": "^1.4.0",
14
+ "date-fns": "^2.23.0",
15
+ "klona": "^1.1.2",
16
+ "mysql": "^2.18.1",
17
+ "to-snake-case": "^1.0.0"
18
+ }
19
+ }