@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/README.md +164 -164
- package/chart-helper.js +254 -254
- package/formatter-templates.json +14 -8
- package/index.js +208 -208
- package/package.json +19 -19
- package/schema.md +120 -120
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.
|
|
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
|
+
}
|