@opengis/bi 1.0.13 → 1.0.15
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 +50 -52
- package/config.js +12 -12
- package/dist/bi.js +1 -1
- package/dist/bi.umd.cjs +120 -134
- package/dist/{import-file-1T7kpSzt.js → import-file-CRC0sYYT.js} +11974 -11522
- package/dist/{map-component-mixin-BLM9iEWA.js → map-component-mixin-BCtWEvzv.js} +4830 -3150
- package/dist/style.css +1 -1
- package/dist/vs-calendar-5ot79n0N.js +110 -0
- package/dist/vs-funnel-bar-CLo6gXI_.js +105 -0
- package/dist/vs-heatmap-DHGA8dRk.js +97 -0
- package/dist/{vs-map-cluster-Dfe9INqE.js → vs-map-cluster-CNgX6JVF.js} +28 -25
- package/dist/vs-map-pIn5wS4G.js +74 -0
- package/dist/vs-number-DYfok8VU.js +55 -0
- package/dist/{vs-text-DcrAdQ40.js → vs-text-Dckykz09.js} +19 -13
- package/package.json +107 -72
- package/plugin.js +14 -13
- package/server/migrations/bi.dataset.sql +26 -0
- package/server/migrations/bi.sql +93 -27
- package/server/plugins/docs.js +48 -47
- package/server/plugins/hook.js +89 -86
- package/server/plugins/vite.js +69 -55
- package/server/routes/dashboard/controllers/dashboard.delete.js +38 -35
- package/server/routes/dashboard/controllers/dashboard.js +118 -80
- package/server/routes/dashboard/controllers/dashboard.list.js +30 -39
- package/server/routes/dashboard/controllers/utils/yaml.js +11 -12
- package/server/routes/dashboard/index.mjs +25 -24
- package/server/routes/data/controllers/data.js +168 -97
- package/server/routes/data/controllers/util/chartSQL.js +42 -25
- package/server/routes/data/controllers/util/normalizeData.js +59 -34
- package/server/routes/data/index.mjs +29 -26
- package/server/routes/dataset/controllers/bi.dataset.demo.add.js +97 -0
- package/server/routes/dataset/controllers/bi.dataset.import.js +67 -0
- package/server/routes/dataset/controllers/util/create.table.js +22 -0
- package/server/routes/dataset/controllers/util/prepare.data.js +49 -0
- package/server/routes/dataset/index.mjs +19 -0
- package/server/routes/db/controllers/dbTablePreview.js +63 -0
- package/server/routes/db/controllers/dbTables.js +36 -0
- package/server/routes/db/index.mjs +17 -0
- package/server/routes/edit/controllers/dashboard.add.js +26 -23
- package/server/routes/edit/controllers/dashboard.edit.js +46 -37
- package/server/routes/edit/controllers/widget.add.js +75 -49
- package/server/routes/edit/controllers/widget.del.js +69 -63
- package/server/routes/edit/controllers/widget.edit.js +52 -82
- package/server/routes/edit/index.mjs +31 -27
- package/server/routes/map/controllers/cluster.js +109 -75
- package/server/routes/map/controllers/clusterVtile.js +166 -143
- package/server/routes/map/controllers/geojson.js +127 -101
- package/server/routes/map/controllers/map.js +60 -57
- package/server/routes/map/controllers/utils/downloadClusterData.js +43 -0
- package/server/routes/map/controllers/vtile.js +183 -161
- package/server/routes/map/index.mjs +25 -25
- package/server/utils/getWidget.js +85 -56
- package/utils.js +12 -11
- package/dist/vs-calendar-WiK1hcHS.js +0 -96
- package/dist/vs-funnel-bar-CpPbYZ0_.js +0 -92
- package/dist/vs-heatmap-BG4eIROH.js +0 -83
- package/dist/vs-map-BRk6Fmks.js +0 -66
- package/dist/vs-number-CJq-vi95.js +0 -39
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import yaml from 'js-yaml';
|
|
2
|
-
|
|
3
|
-
yaml.loadSafe = (yml) => {
|
|
4
|
-
try {
|
|
5
|
-
return yaml.load(yml);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export default yaml;
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
|
|
3
|
+
yaml.loadSafe = (yml) => {
|
|
4
|
+
try {
|
|
5
|
+
return yaml.load(yml);
|
|
6
|
+
} catch (err) {
|
|
7
|
+
return { error: err.toString() };
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default yaml;
|
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
import config from '../../../config.js';
|
|
2
|
-
|
|
3
|
-
import dashboard from './controllers/dashboard.js';
|
|
4
|
-
import dashboardList from './controllers/dashboard.list.js';
|
|
5
|
-
import dashboardDelete from './controllers/dashboard.delete.js';
|
|
6
|
-
const biSchema = {
|
|
7
|
-
querystring: {
|
|
8
|
-
widget: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
9
|
-
dashboard: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
10
|
-
list: { type: 'string', pattern: '^([\\d])$' },
|
|
11
|
-
sql: { type: 'string', pattern: '^([\\d])$' }
|
|
12
|
-
},
|
|
13
|
-
params: {
|
|
14
|
-
id: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
fastify.get(`/bi-dashboard
|
|
21
|
-
fastify.
|
|
22
|
-
fastify.
|
|
23
|
-
|
|
24
|
-
}
|
|
1
|
+
import config from '../../../config.js';
|
|
2
|
+
|
|
3
|
+
import dashboard from './controllers/dashboard.js';
|
|
4
|
+
import dashboardList from './controllers/dashboard.list.js';
|
|
5
|
+
import dashboardDelete from './controllers/dashboard.delete.js';
|
|
6
|
+
const biSchema = {
|
|
7
|
+
querystring: {
|
|
8
|
+
widget: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
9
|
+
dashboard: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
10
|
+
list: { type: 'string', pattern: '^([\\d])$' },
|
|
11
|
+
sql: { type: 'string', pattern: '^([\\d])$' },
|
|
12
|
+
},
|
|
13
|
+
params: {
|
|
14
|
+
id: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default async function route(fastify) {
|
|
19
|
+
fastify.get(`/bi-dashboard/:id`, { schema: biSchema }, dashboard);
|
|
20
|
+
fastify.get(`/bi-dashboard`, dashboardList);
|
|
21
|
+
fastify.delete(`/bi-dashboard/:id`, dashboardDelete);
|
|
22
|
+
fastify.get(`/bi-test`, () => {
|
|
23
|
+
return { test: '2' };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -1,97 +1,168 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const {
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
config,
|
|
5
|
+
getPGAsync,
|
|
6
|
+
autoIndex,
|
|
7
|
+
pgClients,
|
|
8
|
+
getSelectVal,
|
|
9
|
+
getFilterSQL,
|
|
10
|
+
} from '@opengis/fastify-table/utils.js';
|
|
11
|
+
|
|
12
|
+
import chartSQL from './util/chartSQL.js';
|
|
13
|
+
import normalizeData from './util/normalizeData.js';
|
|
14
|
+
|
|
15
|
+
import { getWidget } from '../../../../utils.js';
|
|
16
|
+
|
|
17
|
+
export default async function dataAPI({ funcs = {}, query = {} }) {
|
|
18
|
+
const time = Date.now();
|
|
19
|
+
|
|
20
|
+
query.metric = Array.isArray(query.metric) ? query.metric.pop() : query.metric;
|
|
21
|
+
|
|
22
|
+
const { dashboard, widget, filter, search, samples } = query;
|
|
23
|
+
|
|
24
|
+
const widgetData = await getWidget({ dashboard, widget });
|
|
25
|
+
|
|
26
|
+
if (widgetData.status) return widgetData;
|
|
27
|
+
|
|
28
|
+
const { type, text, data = {}, controls, style, options } = widgetData;
|
|
29
|
+
|
|
30
|
+
const pg = data.db ? await getPGAsync(data.db) : pgClients.client;
|
|
31
|
+
|
|
32
|
+
const { fields: cols } = await pg.query(
|
|
33
|
+
`select * from ${data.table} limit 0`
|
|
34
|
+
);
|
|
35
|
+
const columnTypes = cols.map((el) => ({
|
|
36
|
+
name: el.name,
|
|
37
|
+
type: pg.pgType?.[el.dataTypeID],
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// data param
|
|
41
|
+
const { x, cls, metric, table, where, tableSQL, groupby, xName, yName, xType, yType } =
|
|
42
|
+
normalizeData(widgetData, query, columnTypes);
|
|
43
|
+
|
|
44
|
+
// auto Index
|
|
45
|
+
if (pg.pk?.[data.table]) {
|
|
46
|
+
autoIndex({
|
|
47
|
+
table: data.table,
|
|
48
|
+
pg,
|
|
49
|
+
columns: [data?.time]
|
|
50
|
+
.concat([xName])
|
|
51
|
+
.concat([groupby])
|
|
52
|
+
.filter((el) => el),
|
|
53
|
+
}).catch((err) => console.log(err));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// get group
|
|
57
|
+
const groupData = groupby
|
|
58
|
+
? await pg
|
|
59
|
+
.query(
|
|
60
|
+
`select ${groupby} as name ,count(*) from ${tableSQL || table} group by ${groupby} order by count(*) desc limit 20`
|
|
61
|
+
)
|
|
62
|
+
.then((el) => el.rows)
|
|
63
|
+
: null;
|
|
64
|
+
|
|
65
|
+
if (query.sql === '2') return { x, metric, table, tableSQL, data, groupData };
|
|
66
|
+
|
|
67
|
+
const order = data.order || (type === 'listbar' ? 'metric desc' : null);
|
|
68
|
+
|
|
69
|
+
const { optimizedSQL = `select * from ${tableSQL || table}` } =
|
|
70
|
+
filter || search
|
|
71
|
+
? await getFilterSQL({
|
|
72
|
+
pg,
|
|
73
|
+
table,
|
|
74
|
+
filter,
|
|
75
|
+
search,
|
|
76
|
+
})
|
|
77
|
+
: {};
|
|
78
|
+
|
|
79
|
+
if (type?.includes('bar') && !metric?.length) {
|
|
80
|
+
return { message: 'empty widget params: metrics', status: 400 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sql = (chartSQL[type] || chartSQL.chart)({
|
|
84
|
+
where,
|
|
85
|
+
metric,
|
|
86
|
+
table: `(${optimizedSQL} ${samples ? 'limit 10' : ''})q`,
|
|
87
|
+
x,
|
|
88
|
+
groupData,
|
|
89
|
+
groupby,
|
|
90
|
+
order,
|
|
91
|
+
samples,
|
|
92
|
+
xType,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (query.sql) return sql;
|
|
96
|
+
|
|
97
|
+
if (!sql || sql?.includes('undefined')) {
|
|
98
|
+
return {
|
|
99
|
+
message: {
|
|
100
|
+
error: 'invalid sql',
|
|
101
|
+
type,
|
|
102
|
+
sql,
|
|
103
|
+
where,
|
|
104
|
+
metric,
|
|
105
|
+
table: `(${optimizedSQL})q`,
|
|
106
|
+
x,
|
|
107
|
+
groupData,
|
|
108
|
+
groupby,
|
|
109
|
+
},
|
|
110
|
+
status: 500,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { rows, fields } = await pg.query(sql); // test with limit
|
|
115
|
+
|
|
116
|
+
if (cls) {
|
|
117
|
+
const values = rows
|
|
118
|
+
.map((row) => row[x])
|
|
119
|
+
?.filter((el, idx, arr) => el && arr.indexOf(el) === idx);
|
|
120
|
+
const vals = await getSelectVal({ pg, name: cls, values });
|
|
121
|
+
rows
|
|
122
|
+
.filter((row) => row[x])
|
|
123
|
+
.forEach((row) => {
|
|
124
|
+
Object.assign(row, { [x]: vals?.[row[x]] || row[x] });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const yml = widgetData.yml || yaml.dump(extractYml(widgetData));
|
|
129
|
+
const dimensions = fields.map((el) => el.name);
|
|
130
|
+
|
|
131
|
+
const res = {
|
|
132
|
+
time: Date.now() - time,
|
|
133
|
+
dimensions,
|
|
134
|
+
filter: xName,
|
|
135
|
+
dimensionsType: [xType, yType].filter((el) => el)?.length
|
|
136
|
+
? [xType, yType].filter((el) => el)
|
|
137
|
+
: fields.map((el) => pg.pgType?.[el.dataTypeID]),
|
|
138
|
+
type,
|
|
139
|
+
|
|
140
|
+
text: text || widgetData?.title || data.text,
|
|
141
|
+
// data: query.format === 'data' ? dimensions.map(el => rows.map(r => r[el])) : undefined,
|
|
142
|
+
source:
|
|
143
|
+
query.format === 'array'
|
|
144
|
+
? dimensions.map((el) => rows.map((r) => r[el]))
|
|
145
|
+
: rows,
|
|
146
|
+
style,
|
|
147
|
+
options,
|
|
148
|
+
controls,
|
|
149
|
+
yml,
|
|
150
|
+
params: config?.local ? {
|
|
151
|
+
x,
|
|
152
|
+
cls,
|
|
153
|
+
metric,
|
|
154
|
+
table,
|
|
155
|
+
tableSQL,
|
|
156
|
+
where,
|
|
157
|
+
groupby,
|
|
158
|
+
sql,
|
|
159
|
+
} : undefined,
|
|
160
|
+
columnTypes,
|
|
161
|
+
};
|
|
162
|
+
return res;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function extractYml(sourceData) {
|
|
166
|
+
const { title, description, type, data, style, controls } = sourceData;
|
|
167
|
+
return { title, description, type, data, style, controls };
|
|
168
|
+
}
|
|
@@ -1,25 +1,42 @@
|
|
|
1
|
-
function number({ metric, where, table, samples }) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
function table({ columns, table, where, samples }) {
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function chart({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
function number({ metric, where, table, samples }) {
|
|
2
|
+
const sql = `select ${metric} from ${table} where ${where} ${samples ? 'limit 10' : ''}`;
|
|
3
|
+
return sql;
|
|
4
|
+
}
|
|
5
|
+
function table({ columns, table, where, samples }) {
|
|
6
|
+
return `select ${columns.map((el) => el.name || el)}::text from ${table} where ${where} ${samples ? 'limit 10' : 'limit 20'} `;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function chart({
|
|
10
|
+
metric,
|
|
11
|
+
where,
|
|
12
|
+
table,
|
|
13
|
+
x,
|
|
14
|
+
groupby,
|
|
15
|
+
groupData,
|
|
16
|
+
order,
|
|
17
|
+
samples,
|
|
18
|
+
xType,
|
|
19
|
+
}) {
|
|
20
|
+
const xCol = x && xType?.includes('[]') ? `unnest(${x})` : x;
|
|
21
|
+
|
|
22
|
+
const metricData =
|
|
23
|
+
groupData
|
|
24
|
+
?.map(
|
|
25
|
+
(el) =>
|
|
26
|
+
`${metric} filter (where ${groupby}='${el.name}') as "${el.name}"`
|
|
27
|
+
)
|
|
28
|
+
.join(',') || `${metric} as metric`;
|
|
29
|
+
const sql = `select ${xCol}, ${metricData}
|
|
30
|
+
from ${table}
|
|
31
|
+
where ${where}
|
|
32
|
+
${xCol ? `group by ${xCol}` : ''}
|
|
33
|
+
${order || xCol ? `order by ${order || xCol}` : ''}
|
|
34
|
+
${samples ? 'limit 10' : 'limit 100'}`;
|
|
35
|
+
return sql;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function text() {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default { number, chart };
|
|
@@ -1,34 +1,59 @@
|
|
|
1
|
-
function normalizeData(data, query = {}, columnTypes = []) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
function normalizeData(data, query = {}, columnTypes = []) {
|
|
2
|
+
['x', 'groupby', 'granularity'].forEach((el) => {
|
|
3
|
+
// console.log(el, query[el], columnTypes.find(col => col.name == query[el]))
|
|
4
|
+
if (!columnTypes.find((col) => col.name == query[el])) {
|
|
5
|
+
delete query[el];
|
|
6
|
+
}
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
if (
|
|
10
|
+
!columnTypes.find(
|
|
11
|
+
(col) => col.type === 'numeric' && col.name == query.metric
|
|
12
|
+
)
|
|
13
|
+
) {
|
|
14
|
+
delete query.metric;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const xName = query.x || (Array.isArray(data.x) ? data.x[0] : data.x);
|
|
18
|
+
const xType = columnTypes.find((el) => el.name == xName)?.type;
|
|
19
|
+
|
|
20
|
+
const granularity =
|
|
21
|
+
xType === 'date' || xType?.includes('timestamp')
|
|
22
|
+
? query.granularity || data.granularity || 'year'
|
|
23
|
+
: null;
|
|
24
|
+
|
|
25
|
+
const x =
|
|
26
|
+
(granularity
|
|
27
|
+
? `date_trunc('${granularity}',${xName})::date::text`
|
|
28
|
+
: null) || xName;
|
|
29
|
+
|
|
30
|
+
const metrics = Array.isArray(data.metrics) ? data.metrics : [data.metrics];
|
|
31
|
+
const metric =
|
|
32
|
+
(query.metric ? `sum(${query.metric})` : null) ||
|
|
33
|
+
(metrics.length
|
|
34
|
+
? (metrics
|
|
35
|
+
?.filter((el) => el && columnTypes.find((col) => col.name == (el?.name || el)))
|
|
36
|
+
?.map((el) => el.fx || `${el.operator || 'sum'}(${el.name || el})`)?.join(',') || 'count(*)')
|
|
37
|
+
: 'count(*)');
|
|
38
|
+
|
|
39
|
+
const yName = metrics?.[0]?.name || metrics?.[0];
|
|
40
|
+
const yType = columnTypes.find((el) => el.name == yName)?.type;
|
|
41
|
+
|
|
42
|
+
const { cls, table, filterCustom } = data;
|
|
43
|
+
const groupby = query.groupby || data.groupby;
|
|
44
|
+
// const orderby = query.orderby || data.orderby || 'count(*)';
|
|
45
|
+
|
|
46
|
+
const custom = query?.filterCustom
|
|
47
|
+
?.split(',')
|
|
48
|
+
?.map((el) => filterCustom?.find((item) => item?.name === el)?.sql)
|
|
49
|
+
?.filter((el) => el)
|
|
50
|
+
?.join(' and ');
|
|
51
|
+
const where = `${data.query || '1=1'} and ${custom || 'true'}`;
|
|
52
|
+
|
|
53
|
+
const tableSQL = data.tableSQL?.length
|
|
54
|
+
? `(select * from ${data?.table} t ${data.tableSQL.join(' \n ')} where ${where})q`
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
return { x, cls, metric, table, where, tableSQL, groupby, xName, xType, yName, yType };
|
|
58
|
+
}
|
|
59
|
+
export default normalizeData;
|
|
@@ -1,26 +1,29 @@
|
|
|
1
|
-
import config from '../../../config.js';
|
|
2
|
-
|
|
3
|
-
import data from './controllers/data.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
import config from '../../../config.js';
|
|
2
|
+
|
|
3
|
+
import data from './controllers/data.js';
|
|
4
|
+
|
|
5
|
+
const biSchema = {
|
|
6
|
+
querystring: {
|
|
7
|
+
widget: { type: 'string', pattern: '^([\\d\\w_]+)$' },
|
|
8
|
+
dashboard: { type: 'string', pattern: '^([\\d\\w_]+)$' },
|
|
9
|
+
sql: { type: 'string', pattern: '^([\\d])$' },
|
|
10
|
+
// metric: { type: 'string', pattern: '^([\\d\\w_]+)$' },
|
|
11
|
+
x: { type: 'string', pattern: '^([\\d\\w_]+)$' },
|
|
12
|
+
granularity: { type: 'string', pattern: '^(week|month|quarter|year)$' },
|
|
13
|
+
groupby: { type: 'string', pattern: '^([\\d\\w_]+)$' },
|
|
14
|
+
filterCustom: { type: 'string', pattern: '^([\\d\\w_,]+)$' },
|
|
15
|
+
},
|
|
16
|
+
params: {
|
|
17
|
+
id: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default async function route(fastify, opts) {
|
|
22
|
+
const prefix = opts?.prefix || config.prefix || '/api';
|
|
23
|
+
fastify.route({
|
|
24
|
+
method: 'GET',
|
|
25
|
+
url: '/bi-data',
|
|
26
|
+
schema: biSchema,
|
|
27
|
+
handler: data,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import { getPG } from '@opengis/fastify-table/utils.js';
|
|
5
|
+
|
|
6
|
+
const host = 'https://cdn.softpro.ua/demo/bi';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const root = process.cwd();
|
|
10
|
+
const send = (msg) => console.log(msg)
|
|
11
|
+
async function downloadFile(fileName) {
|
|
12
|
+
const pg = getPG();
|
|
13
|
+
send(`${fileName} - checking if already downloaded...`);
|
|
14
|
+
|
|
15
|
+
const demoPath = path.join(root, '/log/temp/', fileName);
|
|
16
|
+
// const fileExists = await isFileExists(demoPath);
|
|
17
|
+
|
|
18
|
+
// send(`${host}/${fileName}` + demoPath)
|
|
19
|
+
const response = await fetch(`${host}/${fileName}`);
|
|
20
|
+
const body = await response.text();
|
|
21
|
+
//send(body)
|
|
22
|
+
if (response?.status !== 200) {
|
|
23
|
+
return { [fileName]: { result: 'file not found', status: 404 } };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await mkdir(path.dirname(demoPath), { recursive: true });
|
|
27
|
+
|
|
28
|
+
await writeFile(demoPath, body, 'utf8');
|
|
29
|
+
|
|
30
|
+
await pg.query(`drop table if exists ${path.parse(fileName).name}`);
|
|
31
|
+
const { exists } = await pg.query(`select to_regclass($1) is not null as exists`, [path.parse(fileName).name])
|
|
32
|
+
.then((res1) => res1.rows?.[0] || {});
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if (exists) {
|
|
36
|
+
send(`${fileName} - dataset table already exists!`);
|
|
37
|
+
return { [fileName]: { result: 'dataset table already exists', status: 200 } };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sql = await readFile(demoPath, 'utf8');
|
|
41
|
+
await pg.query(sql);
|
|
42
|
+
return { [fileName]: { result: 'success', status: 200 } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Імпорт demo даних dashboard
|
|
47
|
+
*
|
|
48
|
+
* @method GET
|
|
49
|
+
* @summary Імпорт demo даних dashboard
|
|
50
|
+
* @priority 4
|
|
51
|
+
* @type api
|
|
52
|
+
* @tag bi
|
|
53
|
+
* @errors 400,500
|
|
54
|
+
* @returns {Number} status Номер помилки
|
|
55
|
+
* @returns {String} error Опис помилки
|
|
56
|
+
* @returns {Object} rows Масив з колонками таблиці
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
const demoList = {
|
|
60
|
+
'demo.orders.sql': 'Статистика продажів (demo)',
|
|
61
|
+
'demo.cleaned_sales_data.sql': 'Статистика продажів (demo 2)',
|
|
62
|
+
'demo.video_game_sales.sql': 'Статистика продажів відеоігор (demo)',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default async function biDatasetDemoAdd(req) {
|
|
66
|
+
const { pg, query = {} } = req;
|
|
67
|
+
// const send = () => { }; // eventStream?
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const sqlList = [];
|
|
72
|
+
|
|
73
|
+
const tables = Object.keys(demoList)?.map((el) => {
|
|
74
|
+
const table = path.parse(el).name;
|
|
75
|
+
sqlList.push(`insert into bi.dataset(dataset_id,name,table_name)
|
|
76
|
+
select '${el}', '${demoList?.[el]}', '${table}' on conflict(dataset_id) do update set
|
|
77
|
+
name=excluded.name, table_name=excluded.table_name
|
|
78
|
+
returning dataset_id as id`);
|
|
79
|
+
return table;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const { count } = await pg.query(`select count(*) from pg_catalog.pg_tables where schemaname||'.'||tablename = any($1)`, [tables]).then((res1) => res1.rows?.[0] || {});
|
|
83
|
+
|
|
84
|
+
if (count === '3' && !query.nocache) {
|
|
85
|
+
const result = await pg.query(sqlList.join(';\n'));
|
|
86
|
+
const ids = (Array.isArray(result) ? result : [result]).filter((el) => el).map((el) => el?.rows?.[0]?.id);
|
|
87
|
+
return { message: { ids, message: 'all datasets already downloaded' }, status: 200 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const res = {};
|
|
91
|
+
const resultAll = await Promise.all(Object.keys(demoList).map(async (fileName) => await downloadFile(fileName))).catch(err => console.log(err));
|
|
92
|
+
|
|
93
|
+
const result = await pg.query(sqlList.join(';\n'));
|
|
94
|
+
const ids = (Array.isArray(result) ? result : [result]).filter((el) => el).map((el) => el?.rows?.[0]?.id);
|
|
95
|
+
return { message: { ids, message: res, resultAll }, status: 200 };
|
|
96
|
+
|
|
97
|
+
};
|