@opengis/bi 1.2.0 → 1.2.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/dist/bi.js +1 -1
- package/dist/bi.umd.cjs +1 -1
- package/dist/{import-file-Bx4xpxVb.js → import-file-D06AZEtP.js} +11 -11
- package/dist/{vs-funnel-bar-C_TceUrc.js → vs-funnel-bar-C8m-602x.js} +1 -1
- package/dist/{vs-list-DyhLUIPb.js → vs-list-BJ0NjSm5.js} +1 -1
- package/dist/{vs-map-BtQJNN4L.js → vs-map-PLlJqaaW.js} +2 -2
- package/dist/{vs-map-cluster-BbPUosvt.js → vs-map-cluster-9tV6eiDA.js} +2 -2
- package/dist/{vs-number-D2GkU586.js → vs-number-B2V_BPer.js} +1 -1
- package/dist/{vs-table-D_Yn9QqB.js → vs-table-WGE9jyDq.js} +1 -1
- package/dist/{vs-text-BivVd6cY.js → vs-text-DkLKRC7F.js} +1 -1
- package/package.json +8 -8
- package/plugin.js +22 -0
- package/server/helpers/mdToHTML.js +17 -0
- package/server/migrations/bi.dataset.sql +46 -0
- package/server/migrations/bi.sql +112 -0
- package/server/plugins/docs.js +48 -0
- package/server/plugins/hook.js +89 -0
- package/server/plugins/vite.js +69 -0
- package/server/routes/dashboard/controllers/dashboard.import.js +103 -0
- package/server/routes/dashboard/controllers/dashboard.js +157 -0
- package/server/routes/dashboard/controllers/dashboard.list.js +40 -0
- package/server/routes/dashboard/controllers/utils/yaml.js +11 -0
- package/server/routes/dashboard/index.mjs +26 -0
- package/server/routes/data/controllers/data.js +229 -0
- package/server/routes/data/controllers/util/chartSQL.js +49 -0
- package/server/routes/data/controllers/util/normalizeData.js +65 -0
- package/server/routes/data/index.mjs +32 -0
- package/server/routes/dataset/controllers/bi.dataset.list.js +29 -0
- package/server/routes/dataset/controllers/bi.db.list.js +19 -0
- package/server/routes/dataset/controllers/comment.js +55 -0
- package/server/routes/dataset/controllers/createDatasetPost.js +134 -0
- package/server/routes/dataset/controllers/data.js +149 -0
- package/server/routes/dataset/controllers/dbTablePreview.js +58 -0
- package/server/routes/dataset/controllers/dbTables.js +34 -0
- package/server/routes/dataset/controllers/delete.js +40 -0
- package/server/routes/dataset/controllers/deleteDataset.js +52 -0
- package/server/routes/dataset/controllers/editDataset.js +90 -0
- package/server/routes/dataset/controllers/export.js +213 -0
- package/server/routes/dataset/controllers/form.js +99 -0
- package/server/routes/dataset/controllers/format.js +46 -0
- package/server/routes/dataset/controllers/insert.js +47 -0
- package/server/routes/dataset/controllers/table.js +68 -0
- package/server/routes/dataset/controllers/update.js +43 -0
- package/server/routes/dataset/index.mjs +132 -0
- package/server/routes/dataset/utils/convertJSONToCSV.js +17 -0
- package/server/routes/dataset/utils/convertJSONToXls.js +47 -0
- package/server/routes/dataset/utils/createTableQuery.js +59 -0
- package/server/routes/dataset/utils/datasetForms.js +1 -0
- package/server/routes/dataset/utils/descriptionList.js +46 -0
- package/server/routes/dataset/utils/downloadRemoteFile.js +58 -0
- package/server/routes/dataset/utils/executeQuery.js +46 -0
- package/server/routes/dataset/utils/getLayersData.js +107 -0
- package/server/routes/dataset/utils/getTableData.js +47 -0
- package/server/routes/dataset/utils/insertDataQuery.js +12 -0
- package/server/routes/dataset/utils/metaFormat.js +24 -0
- package/server/routes/edit/controllers/dashboard.add.js +36 -0
- package/server/routes/edit/controllers/dashboard.delete.js +39 -0
- package/server/routes/edit/controllers/dashboard.edit.js +61 -0
- package/server/routes/edit/controllers/widget.add.js +78 -0
- package/server/routes/edit/controllers/widget.del.js +58 -0
- package/server/routes/edit/controllers/widget.edit.js +106 -0
- package/server/routes/edit/index.mjs +33 -0
- package/server/routes/map/controllers/cluster.js +125 -0
- package/server/routes/map/controllers/clusterVtile.js +166 -0
- package/server/routes/map/controllers/geojson.js +127 -0
- package/server/routes/map/controllers/heatmap.js +118 -0
- package/server/routes/map/controllers/map.js +69 -0
- package/server/routes/map/controllers/utils/downloadClusterData.js +45 -0
- package/server/routes/map/controllers/vtile.js +183 -0
- package/server/routes/map/index.mjs +32 -0
- package/server/templates/page/login.html +59 -0
- package/server/utils/getWidget.js +117 -0
- package/utils.js +12 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
config, logger, pgClients, getTemplate, getTemplatePath, dataInsert, dataUpdate,
|
|
5
|
+
} from '@opengis/fastify-table/utils.js';
|
|
6
|
+
|
|
7
|
+
export default async function dashboardImport({ pg = pgClients.client, query = {}, user = {} }, reply) {
|
|
8
|
+
const time = Date.now();
|
|
9
|
+
|
|
10
|
+
if (!query?.dashboard) {
|
|
11
|
+
return reply.status(400).send('not enough query params: dashboard');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const list = getTemplatePath('dashboard').filter(el => el[0] === query.dashboard);
|
|
15
|
+
|
|
16
|
+
if (!list?.length) {
|
|
17
|
+
return reply.status(404).send(`dashboard not found`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const client = await pg.connect();
|
|
21
|
+
Object.assign(client, {
|
|
22
|
+
options: pg.options,
|
|
23
|
+
pk: pg.pk,
|
|
24
|
+
pgType: pg.pgType,
|
|
25
|
+
tlist: pg.tlist,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const ids = [];
|
|
29
|
+
try {
|
|
30
|
+
await client.query('BEGIN');
|
|
31
|
+
const rows = await Promise.all(list.map(async ([filename]) => {
|
|
32
|
+
|
|
33
|
+
const obj = await getTemplate('dashboard', filename);
|
|
34
|
+
const index = obj?.find((el) => el[0] === 'index.yml')?.[1];
|
|
35
|
+
const { db = pg.options?.database, description, filters, panels, table_name, title, tags, style, grid } = index || {};
|
|
36
|
+
|
|
37
|
+
const dashboardId = await dataInsert({
|
|
38
|
+
pg,
|
|
39
|
+
table: 'bi.dashboard',
|
|
40
|
+
data: {
|
|
41
|
+
db,
|
|
42
|
+
name: filename,
|
|
43
|
+
description,
|
|
44
|
+
filters,
|
|
45
|
+
panels,
|
|
46
|
+
table_name,
|
|
47
|
+
title,
|
|
48
|
+
words: tags?.join(','),
|
|
49
|
+
style,
|
|
50
|
+
grid,
|
|
51
|
+
source: 'file',
|
|
52
|
+
},
|
|
53
|
+
uid: user?.uid,
|
|
54
|
+
}).then(el => el.rows?.[0]?.dashboard_id);
|
|
55
|
+
ids.push(dashboardId);
|
|
56
|
+
|
|
57
|
+
const widgetList = panels?.reduce((acc, curr) => {
|
|
58
|
+
curr.widgets?.forEach(item => acc.push(item));
|
|
59
|
+
if (curr.widget) acc.push(curr.widget);
|
|
60
|
+
return acc;
|
|
61
|
+
}, []).filter(el => el) || [];
|
|
62
|
+
|
|
63
|
+
const widgets = widgetList.length
|
|
64
|
+
? await Promise.all(obj.filter(el => widgetList.includes(path.parse(el[0]).name)).map(async ([widget, widgetData]) => {
|
|
65
|
+
const { ext, name } = path.parse(widget);
|
|
66
|
+
const data = ext === '.yml' ? widgetData : { type: 'text', data: { text: widgetData } };
|
|
67
|
+
Object.assign(data, { name, x: widgetData?.data?.x, table_name: widgetData?.data?.table, dashboard_id: dashboardId });
|
|
68
|
+
|
|
69
|
+
const res = await dataInsert({
|
|
70
|
+
pg: client,
|
|
71
|
+
table: 'bi.widget',
|
|
72
|
+
data,
|
|
73
|
+
uid: user?.uid,
|
|
74
|
+
}).then(el => el.rows?.[0] || {});
|
|
75
|
+
return res;
|
|
76
|
+
}))
|
|
77
|
+
: [];
|
|
78
|
+
const test = await dataUpdate({
|
|
79
|
+
pg: client,
|
|
80
|
+
table: 'bi.dashboard',
|
|
81
|
+
id: dashboardId,
|
|
82
|
+
data: {
|
|
83
|
+
widgets,
|
|
84
|
+
},
|
|
85
|
+
uid: user?.uid,
|
|
86
|
+
});
|
|
87
|
+
return { dashboardId, name: filename, widgets };
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
await client.query('COMMIT');
|
|
91
|
+
return { time: Date.now() - time, rows };
|
|
92
|
+
} catch (err) {
|
|
93
|
+
await client.query('ROLLBACK');
|
|
94
|
+
if (ids.length) {
|
|
95
|
+
const { rowCount = 0 } = await pg.query('delete from bi.dashboard where dashboard_id = any($1)', [ids]);
|
|
96
|
+
console.log('ROLLBACK delete', rowCount, 'dashboard');
|
|
97
|
+
// const { rowCount: rowCount1 = 0 } = await pg.query('delete from bi.widget where dashboard_id = any($1)', [ids]);
|
|
98
|
+
// console.log('delete', rowCount1, 'widget');
|
|
99
|
+
}
|
|
100
|
+
logger.file('bi/dashboardImport/error', { error: err.toString(), stack: err.stack, ids });
|
|
101
|
+
return reply.status(500).send(config.debug ? err.toString() : 'import error');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
import {
|
|
3
|
+
pgClients,
|
|
4
|
+
getTemplatePath,
|
|
5
|
+
getTemplate,
|
|
6
|
+
getPGAsync,
|
|
7
|
+
getMeta,
|
|
8
|
+
} from '@opengis/fastify-table/utils.js';
|
|
9
|
+
|
|
10
|
+
export default async function dashboard({
|
|
11
|
+
pg: pg1 = pgClients.client, params = {},
|
|
12
|
+
}) {
|
|
13
|
+
const time = Date.now();
|
|
14
|
+
const { id } = params;
|
|
15
|
+
|
|
16
|
+
if (!id) {
|
|
17
|
+
return { message: 'not enough params: dashboard required', status: 400 };
|
|
18
|
+
}
|
|
19
|
+
const dashboards = getTemplatePath('dashboard');
|
|
20
|
+
|
|
21
|
+
const fileDashboard = dashboards.find((el) => el[0] === id);
|
|
22
|
+
if (!fileDashboard) {
|
|
23
|
+
const sql = `select title, description, table_name, panels, grid, widgets, filters, style, words, db, public
|
|
24
|
+
from bi.dashboard where $1 in (dashboard_id, name)`;
|
|
25
|
+
|
|
26
|
+
const data = await pg1.query(sql, [id]).then(el => el.rows?.[0] || {});
|
|
27
|
+
|
|
28
|
+
let pg = pg1;
|
|
29
|
+
try {
|
|
30
|
+
pg = data.pg || (data.db ? await getPGAsync(data.db) : null) || pg1;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
data.error = err.toString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
data.type = 'bd';
|
|
36
|
+
const { table_name: table } = data;
|
|
37
|
+
|
|
38
|
+
if (!table) {
|
|
39
|
+
return { message: 'not enough params: table required', status: 400 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const loadTemplate = pg.pk?.['admin.doc_template'] ? await pg.query(
|
|
43
|
+
'select body from admin.doc_template where doc_type=5 and title=$1',
|
|
44
|
+
[table]
|
|
45
|
+
).then(el => el.rows?.[0]?.body) : null;
|
|
46
|
+
|
|
47
|
+
const sqlList = loadTemplate?.sql ? loadTemplate?.sql
|
|
48
|
+
?.filter?.(el => !el.disabled && el?.sql?.replace)
|
|
49
|
+
?.map?.((el, i) => `left join lateral (${el.sql.replace(/limit 1/ig, '')}) t${i} on 1=1`)?.join?.(' ') : '';
|
|
50
|
+
|
|
51
|
+
const { fields = [] } = table && pg.pk?.[loadTemplate?.table || table] ? await pg.query(`select * from ${loadTemplate?.table || table} t ${sqlList || ''} limit 0`) : {};
|
|
52
|
+
|
|
53
|
+
data?.widgets?.forEach?.(el => {
|
|
54
|
+
const { style, data = {}, type, title, x, metrics } = el;
|
|
55
|
+
el.yml = yaml.dump({ title, type, data: { x, metrics, ...data }, style, })
|
|
56
|
+
// el.yml = yaml.dump({ style, data, type, title });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
data.panels?.forEach?.(el => {
|
|
60
|
+
const { title, type } = data?.widgets?.find(item => item.name === el.widget) || {};
|
|
61
|
+
Object.assign(el, { title, type });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const meta = table ? await getMeta({ pg, table: loadTemplate?.table || table }) : {};
|
|
65
|
+
const columnIndexes = meta?.columns?.reduce((acc, curr, idx) => Object.assign(acc, { [curr.name]: idx }), {}) || [];
|
|
66
|
+
|
|
67
|
+
const columns = fields?.map(el => ({ name: el.name, title: meta?.columns[columnIndexes[el.name]]?.title || el.name, type: pg.pgType?.[el.dataTypeID] }));
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...data || {},
|
|
71
|
+
widgets: data?.widgets?.filter?.(el => el?.widget_id) || [],
|
|
72
|
+
panels: data?.panels?.filter?.(el => el?.widget) || [],
|
|
73
|
+
geom: !meta?.geom,
|
|
74
|
+
error:
|
|
75
|
+
table && !pg.pk?.[loadTemplate?.table || table] ? `table pkey not found: ${loadTemplate?.table || table}` : undefined,
|
|
76
|
+
table_name: table,
|
|
77
|
+
templateTable: loadTemplate?.table,
|
|
78
|
+
time: Date.now() - time,
|
|
79
|
+
columns,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const fileData = await getTemplate('dashboard', id);
|
|
84
|
+
const index = fileData.find((el) => el[0] === 'index.yml')[1];
|
|
85
|
+
|
|
86
|
+
if (!index) {
|
|
87
|
+
return { message: `not found ${id}`, status: 404 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const data = index;
|
|
91
|
+
data.type = 'file';
|
|
92
|
+
const { table } = data?.data || { table: data?.table_name };
|
|
93
|
+
|
|
94
|
+
let pg = pg1;
|
|
95
|
+
try {
|
|
96
|
+
pg = data.pg || (data.db ? await getPGAsync(data.db) : null) || pg1;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
data.error = err.toString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { fields = [] } = table ? await pg.query(`select * from ${table} limit 0`) : {};
|
|
102
|
+
|
|
103
|
+
const meta = await getMeta({ pg: pg1, table });
|
|
104
|
+
|
|
105
|
+
const columns = meta?.columns
|
|
106
|
+
?.filter(el => fields.map(field => field.name).includes(el.name))
|
|
107
|
+
?.map(el => ({ name: el.name, title: el.title, type: pg.pgType?.[el.dataTypeID] }));
|
|
108
|
+
|
|
109
|
+
const checks = index?.filters?.filter((el) => el?.id && fields.map((el) => el?.name).includes(el?.id) && el?.type === 'Check');
|
|
110
|
+
if (checks?.length) {
|
|
111
|
+
await Promise.all(checks.map(async (el) => {
|
|
112
|
+
if (el?.data) {
|
|
113
|
+
const options = await getTemplate('cls', el.data);
|
|
114
|
+
Object.assign(el, { options });
|
|
115
|
+
} else if (index?.table_name || index?.table) {
|
|
116
|
+
const { rows = [] } = await pg.query(`select "${el.id}" as id, count(*) from ${index?.table_name || index?.table} group by "${el.id}"`);
|
|
117
|
+
Object.assign(el, { options: rows });
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
const ranges = index?.filters?.filter((el) => el?.id && fields.map((el) => el?.name).includes(el?.id) && el?.type === 'Range');
|
|
122
|
+
if (ranges?.length) {
|
|
123
|
+
await Promise.all(ranges.map(async (el) => {
|
|
124
|
+
const rows = await pg.query(`select array[
|
|
125
|
+
percentile_cont(0) within group (order by ${el.id}), percentile_cont(0.25) within group (order by ${el.id}),
|
|
126
|
+
percentile_cont(0.5) within group (order by ${el.id}), percentile_cont(0.75) within group (order by ${el.id}),
|
|
127
|
+
percentile_cont(1.0) within group (order by ${el.id})
|
|
128
|
+
] from ${index?.table_name || index?.table}`).then(el => el.rows?.[0]?.array || []);
|
|
129
|
+
Object.assign(el, { options: rows });
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// console.log(fileData)
|
|
134
|
+
const widgets = fileData
|
|
135
|
+
.filter((el) => el[0] !== 'index.yml')
|
|
136
|
+
.map((el) =>
|
|
137
|
+
el[1].data
|
|
138
|
+
? {
|
|
139
|
+
name: el[0].split('.')[0],
|
|
140
|
+
type: el[1].type,
|
|
141
|
+
title: el[1].title,
|
|
142
|
+
style: el[1].style,
|
|
143
|
+
data: el[1].data,
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
: { name: el[0].split('.')[0], title: el[1] }
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
...data,
|
|
151
|
+
geom: !!meta?.geom,
|
|
152
|
+
table_name: table,
|
|
153
|
+
time: Date.now() - time,
|
|
154
|
+
columns,
|
|
155
|
+
widgets,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { pgClients, getTemplatePath, getTemplate } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
3
|
+
const q = `select dashboard_id as name, 'db' as type, title, description, table_name, words, public from bi.dashboard`;
|
|
4
|
+
|
|
5
|
+
export default async function data({
|
|
6
|
+
pg = pgClients.client, query = {}, user = {}
|
|
7
|
+
}) {
|
|
8
|
+
const time = Date.now();
|
|
9
|
+
const { type = 'file' } = query;
|
|
10
|
+
|
|
11
|
+
const data = getTemplatePath('dashboard');
|
|
12
|
+
const dir = type === 'file' ? await Promise.all(
|
|
13
|
+
data.map(async ([filename, filepath]) => {
|
|
14
|
+
const obj = await getTemplate('dashboard', filename);
|
|
15
|
+
const index = obj?.find?.((el) => el[0] === 'index.yml')?.[1];
|
|
16
|
+
const { table_name, description, title } = index || {};
|
|
17
|
+
|
|
18
|
+
return { name: filename, path: user?.user_type?.includes('admin') ? filepath : undefined, type: 'file', title, description, table_name };
|
|
19
|
+
})
|
|
20
|
+
) : [];
|
|
21
|
+
|
|
22
|
+
const { rows = [] } = ['db', 'viewer'].includes(type) ? await pg.query(q) : {};
|
|
23
|
+
|
|
24
|
+
if (type === 'viewer') {
|
|
25
|
+
return {
|
|
26
|
+
time: Date.now() - time,
|
|
27
|
+
db: pg.options?.database,
|
|
28
|
+
rows: rows.filter(el => el.table_name && !el.table_name.startsWith('demo.')),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const list = dir.concat(rows);
|
|
33
|
+
|
|
34
|
+
const res = {
|
|
35
|
+
time: Date.now() - time,
|
|
36
|
+
db: pg.options?.database,
|
|
37
|
+
rows: list,
|
|
38
|
+
};
|
|
39
|
+
return res;
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import dashboard from './controllers/dashboard.js';
|
|
2
|
+
import dashboardList from './controllers/dashboard.list.js';
|
|
3
|
+
import dashboardImport from './controllers/dashboard.import.js';
|
|
4
|
+
|
|
5
|
+
const biSchema = {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
querystring: {
|
|
9
|
+
widget: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
10
|
+
dashboard: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
11
|
+
list: { type: 'string', pattern: '^([\\d])$' },
|
|
12
|
+
sql: { type: 'string', pattern: '^([\\d])$' },
|
|
13
|
+
},
|
|
14
|
+
params: {
|
|
15
|
+
id: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const policy = ['public'];
|
|
21
|
+
|
|
22
|
+
export default async function route(fastify) {
|
|
23
|
+
fastify.get(`/bi-dashboard/:id`, { config: { policy }, schema: biSchema }, dashboard);
|
|
24
|
+
fastify.get(`/bi-dashboard`, { config: { policy } }, dashboardList);
|
|
25
|
+
fastify.get(`/bi-dashboard-import`, { config: { policy } }, dashboardImport);
|
|
26
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
config,
|
|
5
|
+
autoIndex,
|
|
6
|
+
pgClients,
|
|
7
|
+
getSelect,
|
|
8
|
+
getSelectVal,
|
|
9
|
+
getFilterSQL,
|
|
10
|
+
getMeta,
|
|
11
|
+
logger,
|
|
12
|
+
} from '@opengis/fastify-table/utils.js';
|
|
13
|
+
|
|
14
|
+
import chartSQL from './util/chartSQL.js';
|
|
15
|
+
import normalizeData from './util/normalizeData.js';
|
|
16
|
+
|
|
17
|
+
import { getWidget } from '../../../../utils.js';
|
|
18
|
+
|
|
19
|
+
const maxLimit = 100;
|
|
20
|
+
|
|
21
|
+
export default async function dataAPI(req, reply) {
|
|
22
|
+
const time = Date.now();
|
|
23
|
+
|
|
24
|
+
const { query = {}, user = {}, unittest } = req;
|
|
25
|
+
|
|
26
|
+
query.metric = Array.isArray(query.metric) ? query.metric.pop() : query.metric;
|
|
27
|
+
|
|
28
|
+
const { dashboard, widget, filter, search, samples } = query;
|
|
29
|
+
|
|
30
|
+
const widgetData = await getWidget({ pg: req.pg, dashboard, widget });
|
|
31
|
+
|
|
32
|
+
if (widgetData.status) return widgetData;
|
|
33
|
+
|
|
34
|
+
const { type, text, data = {}, controls, style, options } = widgetData;
|
|
35
|
+
|
|
36
|
+
const pg = widgetData.pg || req.pg || pgClients.client;
|
|
37
|
+
|
|
38
|
+
const error1 = {};
|
|
39
|
+
const { fields: cols = [] } = await pg.query(
|
|
40
|
+
`select * from ${data.table} t ${widgetData.tableSQL || data.tableSQL || ''} limit 0`
|
|
41
|
+
).catch(err => Object.assign(error1, { error: err.toString() })) || {};
|
|
42
|
+
const columnTypes = cols?.map?.((el) => ({
|
|
43
|
+
name: el.name,
|
|
44
|
+
type: pg.pgType?.[el.dataTypeID],
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
// data param
|
|
48
|
+
const { x, cls, groupbyCls, metric, table, where, tableSQL, groupby, xName, yName, xType, yType, error = error1 } =
|
|
49
|
+
normalizeData(widgetData, query, columnTypes);
|
|
50
|
+
|
|
51
|
+
// if (error) { return reply.status(400).send(error); }
|
|
52
|
+
|
|
53
|
+
// auto Index
|
|
54
|
+
if (pg.pk?.[data.table]) {
|
|
55
|
+
autoIndex({
|
|
56
|
+
table: data.table,
|
|
57
|
+
pg,
|
|
58
|
+
columns: [data?.time]
|
|
59
|
+
.concat([xName])
|
|
60
|
+
.concat([groupby])
|
|
61
|
+
.filter((el) => el),
|
|
62
|
+
}).catch((err) => console.log(err));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { pk, columns = [], view } = await getMeta({ pg, table: data.table });
|
|
66
|
+
|
|
67
|
+
if (!view && !pk) {
|
|
68
|
+
return { message: `table not found: ${data.table} (${pg.options?.database})`, status: 404 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// const columnList = columns.map(col => col.name);
|
|
72
|
+
const groupbyColumnNotExists = groupby?.split?.(',')?.filter?.(el => !columnTypes.map(el => el.name).includes(el.trim()));
|
|
73
|
+
|
|
74
|
+
if (groupby && groupbyColumnNotExists?.length) {
|
|
75
|
+
return { message: `groupby column not found: ${groupbyColumnNotExists} (${data.table}/${pg.options?.database})`, status: 404 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// get group
|
|
79
|
+
const groupData = groupby
|
|
80
|
+
? await pg
|
|
81
|
+
.query(
|
|
82
|
+
`select ${groupby} as name ,count(*) from ${tableSQL || table} group by ${groupby} order by count(*) desc limit 20`
|
|
83
|
+
)
|
|
84
|
+
.then((el) => el.rows)
|
|
85
|
+
: null;
|
|
86
|
+
|
|
87
|
+
if (query.sql === '2') return { x, metric, table, tableSQL, data, groupData };
|
|
88
|
+
|
|
89
|
+
const order = data.order || (type === 'listbar' && cols.find(el => el.name === 'metric') ? 'metric desc' : null);
|
|
90
|
+
|
|
91
|
+
const fData =
|
|
92
|
+
filter || search
|
|
93
|
+
? await getFilterSQL({
|
|
94
|
+
pg,
|
|
95
|
+
table,
|
|
96
|
+
filter,
|
|
97
|
+
search,
|
|
98
|
+
filterList: widgetData.filters,
|
|
99
|
+
})
|
|
100
|
+
: {};
|
|
101
|
+
|
|
102
|
+
const optimizedSQL = widgetData?.sql
|
|
103
|
+
? `${widgetData.sql} ${fData?.q && false ? fData?.q : ''} limit ${Math.min(query.limit || widgetData.limit || maxLimit, maxLimit)}`
|
|
104
|
+
: (fData?.optimizedSQL || `select * from ${tableSQL || table}`);
|
|
105
|
+
|
|
106
|
+
if (type?.includes('bar') && !metric?.length) {
|
|
107
|
+
return { message: 'empty widget params: metrics', status: 400 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sql = widgetData.sql ? optimizedSQL : (chartSQL[type] || chartSQL.chart)({
|
|
111
|
+
where: config.local && user?.user_type === 'superadmin' ? 'true' : where, // test
|
|
112
|
+
metric,
|
|
113
|
+
yType, // metric type
|
|
114
|
+
columns: widgetData.columns,
|
|
115
|
+
table: `(${optimizedSQL})q`,
|
|
116
|
+
x,
|
|
117
|
+
groupData,
|
|
118
|
+
groupby,
|
|
119
|
+
order,
|
|
120
|
+
samples,
|
|
121
|
+
limit: Math.min(query.limit || maxLimit, maxLimit),
|
|
122
|
+
xType,
|
|
123
|
+
fx: widgetData.fx,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (query.sql) return sql;
|
|
127
|
+
|
|
128
|
+
if (!sql || sql?.includes('undefined')) {
|
|
129
|
+
return {
|
|
130
|
+
message: {
|
|
131
|
+
error: 'invalid sql',
|
|
132
|
+
type,
|
|
133
|
+
sql,
|
|
134
|
+
where,
|
|
135
|
+
metric,
|
|
136
|
+
table: `(${optimizedSQL})q`,
|
|
137
|
+
x,
|
|
138
|
+
groupData,
|
|
139
|
+
groupby,
|
|
140
|
+
},
|
|
141
|
+
status: 500,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (config.trace) console.log(sql, user?.uid);
|
|
146
|
+
|
|
147
|
+
const { rows = [], fields = [], errorSql } = await pg.query(sql.replace('{{uid}}', user?.uid)).catch(err => {
|
|
148
|
+
logger.file('bi/data', { error: err.toString(), sql });
|
|
149
|
+
return { errorSql: err.toString() };
|
|
150
|
+
}); // test with limit
|
|
151
|
+
|
|
152
|
+
if (groupbyCls) {
|
|
153
|
+
const { arr = [] } = await getSelect(groupbyCls, pg) || {};
|
|
154
|
+
if (arr.length) {
|
|
155
|
+
const ids = arr.map(el => el.id);
|
|
156
|
+
const text = arr.reduce((acc, curr) => ({ ...acc, [curr.id]: curr.text }), {});
|
|
157
|
+
rows.forEach(row => {
|
|
158
|
+
ids.reduce((acc, curr) => {
|
|
159
|
+
Object.assign(row, { [text[curr]]: row[curr] });
|
|
160
|
+
delete row[curr];
|
|
161
|
+
return acc;
|
|
162
|
+
}, {});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (cls) {
|
|
168
|
+
const values = rows
|
|
169
|
+
.map((row) => row[x])
|
|
170
|
+
?.filter((el, idx, arr) => el && arr.indexOf(el) === idx);
|
|
171
|
+
const vals = await getSelectVal({ pg, name: cls, values });
|
|
172
|
+
rows
|
|
173
|
+
.filter((row) => row[x])
|
|
174
|
+
.forEach((row) => {
|
|
175
|
+
Object.assign(row, { [x]: vals?.[row[x]]?.text || vals?.[row[x]] || row[x] });
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const metaTitles = columns.reduce((acc, curr) => Object.assign(acc, { [curr.name]: curr.title || curr.ua }), {});
|
|
180
|
+
const titles = Array.isArray(widgetData?.columns)
|
|
181
|
+
? widgetData.columns.reduce((acc, curr) => Object.assign(acc, { [curr.name]: curr.title || curr.ua }), {})
|
|
182
|
+
: Object.keys(widgetData?.columns || {}).reduce((acc, curr) => Object.assign(acc, { [curr]: widgetData?.columns?.[curr] }), {});
|
|
183
|
+
|
|
184
|
+
const rows1 = type === 'table' ? rows.map(row => Object.keys(row || {}).reduce((acc, curr) => Object.assign(acc, { [titles?.[curr] || metaTitles?.[curr] || curr]: row?.[curr] }), {})) : rows;
|
|
185
|
+
|
|
186
|
+
const yml = widgetData.yml || yaml.dump(extractYml(widgetData));
|
|
187
|
+
const dimensions = fields.map((el) => el.name);
|
|
188
|
+
|
|
189
|
+
const res = {
|
|
190
|
+
time: Date.now() - time,
|
|
191
|
+
error: error || errorSql || (!widgetData.sql ? widgetData.error : undefined),
|
|
192
|
+
dimensions,
|
|
193
|
+
filter: xName,
|
|
194
|
+
dimensionsType: [xType, yType].filter((el) => el)?.length
|
|
195
|
+
? [xType, yType].filter((el) => el)
|
|
196
|
+
: fields.map((el) => pg.pgType?.[el.dataTypeID]),
|
|
197
|
+
type,
|
|
198
|
+
|
|
199
|
+
text: text || widgetData?.title || data.text,
|
|
200
|
+
// data: query.format === 'data' ? dimensions.map(el => rows.map(r => r[el])) : undefined,
|
|
201
|
+
source:
|
|
202
|
+
query.format === 'array'
|
|
203
|
+
? dimensions.map((el) => rows1.map((r) => r[el]))
|
|
204
|
+
: rows1,
|
|
205
|
+
style,
|
|
206
|
+
options,
|
|
207
|
+
controls,
|
|
208
|
+
yml,
|
|
209
|
+
data: widgetData.data,
|
|
210
|
+
id: query.widget,
|
|
211
|
+
columns: columnTypes.map(el => Object.assign(el, { title: titles[el.name] || metaTitles?.[el.name] || el.name })),
|
|
212
|
+
params: config?.local || unittest ? {
|
|
213
|
+
x,
|
|
214
|
+
cls,
|
|
215
|
+
metric,
|
|
216
|
+
table,
|
|
217
|
+
tableSQL,
|
|
218
|
+
where,
|
|
219
|
+
groupby,
|
|
220
|
+
sql,
|
|
221
|
+
} : undefined,
|
|
222
|
+
};
|
|
223
|
+
return res;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function extractYml(sourceData) {
|
|
227
|
+
const { title, description, type, data, style, controls } = sourceData;
|
|
228
|
+
return { title, description, type, data, style, controls };
|
|
229
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function number({ metric, where, table, samples, fx }) {
|
|
2
|
+
const sql = `select ${fx || metric} from ${table} where ${where} ${samples ? 'limit 10' : ''}`;
|
|
3
|
+
return sql;
|
|
4
|
+
}
|
|
5
|
+
function table({ columns = [], table, where, samples }) {
|
|
6
|
+
const cols = Array.isArray(columns)
|
|
7
|
+
? columns.map((el) => `"${(el.name || el).replace(/'/g, "''")}"`).join(',')
|
|
8
|
+
: Object.keys(columns).map(key => `"${key.replace(/'/g, "''")}"`).join(',');
|
|
9
|
+
return `select ${cols || '*'} from ${table} where ${where} ${samples ? 'limit 10' : 'limit 20'} `;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function chart({
|
|
13
|
+
metric,
|
|
14
|
+
yType, // metric type
|
|
15
|
+
where,
|
|
16
|
+
table,
|
|
17
|
+
x,
|
|
18
|
+
groupby,
|
|
19
|
+
groupData,
|
|
20
|
+
order,
|
|
21
|
+
samples,
|
|
22
|
+
limit = 100,
|
|
23
|
+
xType,
|
|
24
|
+
fx, // agg function
|
|
25
|
+
}) {
|
|
26
|
+
const xCol = x && xType?.includes('[]') ? `unnest(${x})` : x;
|
|
27
|
+
|
|
28
|
+
const metricData =
|
|
29
|
+
groupData
|
|
30
|
+
?.filter(el => el.name)
|
|
31
|
+
?.map(
|
|
32
|
+
(el) =>
|
|
33
|
+
`${metric} filter (where '${el.name.toString().replace(/'/g, "''")}'=${yType?.includes('[]') ? `any(${groupby.replace(/'/g, "''")}::text[])` : groupby.replace(/'/g, "''")}) as "${el.name.toString().replace(/'/g, "''")}"`
|
|
34
|
+
)
|
|
35
|
+
.join(',') || `${fx || metric} as metric`;
|
|
36
|
+
const sql = `select ${xCol} ${x && xType?.includes('[]') ? `as ${x}` : ''}, ${metricData}
|
|
37
|
+
from ${table}
|
|
38
|
+
where ${where}
|
|
39
|
+
${xCol ? `group by ${xCol}` : ''}
|
|
40
|
+
${order || xCol ? `order by ${order || xCol}` : ''}
|
|
41
|
+
${samples ? 'limit 10' : `limit ${limit}`}`;
|
|
42
|
+
return sql;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function text() {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default { number, chart, table };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
function normalizeData(data, query = {}, columnTypes = []) {
|
|
2
|
+
const skip = [];
|
|
3
|
+
['x', 'groupby', 'granularity'].forEach((el) => {
|
|
4
|
+
// console.log(el, query[el], columnTypes.find(col => col.name == query[el]))
|
|
5
|
+
if (!columnTypes.find((col) => col.name == query[el])) {
|
|
6
|
+
if (query[el] && query[el] !== 'null') {
|
|
7
|
+
if (el === 'granularity' && !['week', 'month', 'quarter', 'year'].includes(query[el])) {
|
|
8
|
+
skip.push(`invalid granularity option: ${query[el]}`);
|
|
9
|
+
} else if (el !== 'granularity') { skip.push(`column not found: ${query[el]}`); }
|
|
10
|
+
}
|
|
11
|
+
if (!(el === 'granularity' || (el === 'groupby' && query[el] === 'null'))) delete query[el];
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
!columnTypes.find(
|
|
17
|
+
(col) => col.type === 'numeric' && col.name == query.metric
|
|
18
|
+
)
|
|
19
|
+
) {
|
|
20
|
+
delete query.metric;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const xName = query.x || (Array.isArray(data.x) ? data.x[0] : data.x);
|
|
24
|
+
const xType = columnTypes.find((el) => el.name == xName)?.type;
|
|
25
|
+
|
|
26
|
+
const granularity =
|
|
27
|
+
xType === 'date' || xType?.includes('timestamp')
|
|
28
|
+
? query.granularity || data.granularity || 'year'
|
|
29
|
+
: null;
|
|
30
|
+
|
|
31
|
+
const x =
|
|
32
|
+
(granularity
|
|
33
|
+
? `date_trunc('${granularity}',${xName})::date::text`
|
|
34
|
+
: null) || xName;
|
|
35
|
+
|
|
36
|
+
const metrics = Array.isArray(data.metrics || data.metric) ? (data.metrics || data.metric) : [data.metrics || data.metric];
|
|
37
|
+
const metric =
|
|
38
|
+
(query.metric ? `sum(${query.metric})` : null) ||
|
|
39
|
+
(metrics.length
|
|
40
|
+
? (metrics
|
|
41
|
+
?.filter((el) => el && columnTypes.find((col) => col.name == (el?.name || el)))
|
|
42
|
+
?.map((el) => el.fx || `${el.operator || 'sum'}(${el.name || el})`)?.join(',') || 'count(*)')
|
|
43
|
+
: 'count(*)');
|
|
44
|
+
|
|
45
|
+
const yName = metrics?.[0]?.name || metrics?.[0];
|
|
46
|
+
const yType = columnTypes.find((el) => el.name == yName)?.type;
|
|
47
|
+
|
|
48
|
+
const { cls, groupbyCls, table, filterCustom } = data;
|
|
49
|
+
const groupby = (query.groupby || data.groupby) === 'null' ? null : (query.groupby || data.groupby);
|
|
50
|
+
// const orderby = query.orderby || data.orderby || 'count(*)';
|
|
51
|
+
|
|
52
|
+
const custom = query?.filterCustom
|
|
53
|
+
?.split(',')
|
|
54
|
+
?.map((el) => filterCustom?.find((item) => item?.name === el)?.sql)
|
|
55
|
+
?.filter((el) => el)
|
|
56
|
+
?.join(' and ');
|
|
57
|
+
const where = `${data.query || '1=1'} and ${custom || 'true'}`;
|
|
58
|
+
|
|
59
|
+
const tableSQL = data.tableSQL?.length
|
|
60
|
+
? `(select * from ${data?.table} t ${data.tableSQL || ''} where ${where})q`
|
|
61
|
+
: undefined;
|
|
62
|
+
|
|
63
|
+
return { x, cls, groupbyCls, metric, table, where, tableSQL, groupby, xName, xType, yName, yType, error: skip.length ? skip.join(',') : undefined };
|
|
64
|
+
}
|
|
65
|
+
export default normalizeData;
|