@opengis/bi 1.2.0 → 1.2.2

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.
Files changed (74) hide show
  1. package/dist/bi.js +1 -1
  2. package/dist/bi.umd.cjs +42 -42
  3. package/dist/import-file-D8jh74Dz.js +3543 -0
  4. package/dist/{vs-funnel-bar-C_TceUrc.js → vs-funnel-bar-T330oJNS.js} +3 -3
  5. package/dist/{vs-list-DyhLUIPb.js → vs-list-DeHF_Oaf.js} +109 -109
  6. package/dist/{vs-map-BtQJNN4L.js → vs-map-Skt608pM.js} +8 -8
  7. package/dist/{vs-map-cluster-BbPUosvt.js → vs-map-cluster-BRUiY_90.js} +21 -21
  8. package/dist/{vs-number-D2GkU586.js → vs-number-Dd_21nn-.js} +3 -3
  9. package/dist/{vs-table-D_Yn9QqB.js → vs-table-BwC29Zyc.js} +6 -6
  10. package/dist/{vs-text-BivVd6cY.js → vs-text-DEJjWxDu.js} +32 -39
  11. package/package.json +77 -76
  12. package/plugin.js +22 -0
  13. package/server/helpers/mdToHTML.js +17 -0
  14. package/server/migrations/bi.dataset.sql +46 -0
  15. package/server/migrations/bi.sql +112 -0
  16. package/server/plugins/docs.js +48 -0
  17. package/server/plugins/hook.js +89 -0
  18. package/server/plugins/vite.js +69 -0
  19. package/server/routes/dashboard/controllers/dashboard.import.js +103 -0
  20. package/server/routes/dashboard/controllers/dashboard.js +157 -0
  21. package/server/routes/dashboard/controllers/dashboard.list.js +40 -0
  22. package/server/routes/dashboard/controllers/utils/yaml.js +11 -0
  23. package/server/routes/dashboard/index.mjs +26 -0
  24. package/server/routes/data/controllers/data.js +230 -0
  25. package/server/routes/data/controllers/util/chartSQL.js +49 -0
  26. package/server/routes/data/controllers/util/normalizeData.js +65 -0
  27. package/server/routes/data/index.mjs +32 -0
  28. package/server/routes/dataset/controllers/bi.dataset.list.js +29 -0
  29. package/server/routes/dataset/controllers/bi.db.list.js +19 -0
  30. package/server/routes/dataset/controllers/comment.js +55 -0
  31. package/server/routes/dataset/controllers/createDatasetPost.js +134 -0
  32. package/server/routes/dataset/controllers/data.js +149 -0
  33. package/server/routes/dataset/controllers/dbTablePreview.js +58 -0
  34. package/server/routes/dataset/controllers/dbTables.js +34 -0
  35. package/server/routes/dataset/controllers/delete.js +40 -0
  36. package/server/routes/dataset/controllers/deleteDataset.js +52 -0
  37. package/server/routes/dataset/controllers/editDataset.js +90 -0
  38. package/server/routes/dataset/controllers/export.js +214 -0
  39. package/server/routes/dataset/controllers/form.js +99 -0
  40. package/server/routes/dataset/controllers/format.js +46 -0
  41. package/server/routes/dataset/controllers/insert.js +47 -0
  42. package/server/routes/dataset/controllers/table.js +68 -0
  43. package/server/routes/dataset/controllers/update.js +43 -0
  44. package/server/routes/dataset/index.mjs +132 -0
  45. package/server/routes/dataset/utils/convertJSONToCSV.js +17 -0
  46. package/server/routes/dataset/utils/convertJSONToXls.js +47 -0
  47. package/server/routes/dataset/utils/createTableQuery.js +59 -0
  48. package/server/routes/dataset/utils/datasetForms.js +1 -0
  49. package/server/routes/dataset/utils/descriptionList.js +46 -0
  50. package/server/routes/dataset/utils/downloadRemoteFile.js +58 -0
  51. package/server/routes/dataset/utils/executeQuery.js +46 -0
  52. package/server/routes/dataset/utils/getLayersData.js +107 -0
  53. package/server/routes/dataset/utils/getTableData.js +47 -0
  54. package/server/routes/dataset/utils/insertDataQuery.js +12 -0
  55. package/server/routes/dataset/utils/metaFormat.js +24 -0
  56. package/server/routes/edit/controllers/dashboard.add.js +36 -0
  57. package/server/routes/edit/controllers/dashboard.delete.js +39 -0
  58. package/server/routes/edit/controllers/dashboard.edit.js +61 -0
  59. package/server/routes/edit/controllers/widget.add.js +78 -0
  60. package/server/routes/edit/controllers/widget.del.js +58 -0
  61. package/server/routes/edit/controllers/widget.edit.js +106 -0
  62. package/server/routes/edit/index.mjs +33 -0
  63. package/server/routes/map/controllers/cluster.js +125 -0
  64. package/server/routes/map/controllers/clusterVtile.js +166 -0
  65. package/server/routes/map/controllers/geojson.js +127 -0
  66. package/server/routes/map/controllers/heatmap.js +118 -0
  67. package/server/routes/map/controllers/map.js +69 -0
  68. package/server/routes/map/controllers/utils/downloadClusterData.js +45 -0
  69. package/server/routes/map/controllers/vtile.js +183 -0
  70. package/server/routes/map/index.mjs +32 -0
  71. package/server/templates/page/login.html +59 -0
  72. package/server/utils/getWidget.js +117 -0
  73. package/utils.js +12 -0
  74. package/dist/import-file-Bx4xpxVb.js +0 -3493
@@ -0,0 +1,183 @@
1
+ import Sphericalmercator from '@mapbox/sphericalmercator';
2
+
3
+ import path from 'path';
4
+ import { createHash } from 'crypto';
5
+ import { writeFile, mkdir } from 'fs/promises';
6
+ import { existsSync, /* readdirSync, */ readFileSync } from 'fs';
7
+ import { getWidget } from '../../../../utils.js';
8
+
9
+ import { getFolder, getFilterSQL, logger, pgClients } from '@opengis/fastify-table/utils.js';
10
+
11
+ import normalizeData from '../../data/controllers/util/normalizeData.js';
12
+
13
+ const mercator = new Sphericalmercator({ size: 256 });
14
+
15
+ const types = {
16
+ point: 'ST_Point' /* ,ST_MultiPoint */,
17
+ polygon: 'ST_Polygon,ST_MultiPolygon',
18
+ };
19
+
20
+ const area = {
21
+ 1: 1000000,
22
+ 2: 100000,
23
+ 3: 100000,
24
+ 4: 100000,
25
+ 5: 100000,
26
+ 6: 100000,
27
+ 7: 100000,
28
+ 8: 100000,
29
+ 9: 100000,
30
+ 10: 50000,
31
+ 11: 40000,
32
+ 12: 20000,
33
+ 13: 20000,
34
+ };
35
+
36
+ export default async function vtile(req, reply) {
37
+ const { params = {}, query = {} } = req;
38
+
39
+ const {
40
+ filter,
41
+ widget,
42
+ dashboard,
43
+ sql,
44
+ cluster,
45
+ type,
46
+ nocache,
47
+ geom = 'geom',
48
+ pointZoom = 0,
49
+ } = query;
50
+
51
+ if (!widget) {
52
+ return { message: 'not enough params: widget', status: 400 };
53
+ }
54
+
55
+ const { y, z } = params;
56
+ const x = params.x?.split('.')[0] - 0;
57
+
58
+ if (!x || !y || !z) {
59
+ return { message: 'not enough params: xyz', status: 400 };
60
+ }
61
+
62
+ const { pg = req.pg || pgClients.client, data } = await getWidget({ pg: req.pg, widget, dashboard });
63
+
64
+ const headers = {
65
+ 'Content-Type': 'application/x-protobuf',
66
+ 'Cache-Control': nocache || sql ? 'no-cache' : 'public, max-age=86400',
67
+ };
68
+
69
+ const hash = [pointZoom, filter].filter((el) => el).join();
70
+
71
+ const root = getFolder(req);
72
+ const file = path.join(
73
+ root,
74
+ `/map/vtile/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}${z}/${x}/${y}.mvt`
75
+ );
76
+
77
+ try {
78
+ const pkey = pg.pk?.[data?.table];
79
+ if (!pkey) {
80
+ return {
81
+ message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`,
82
+ status: 400,
83
+ };
84
+ }
85
+
86
+ // data param
87
+ const {
88
+ table,
89
+ where = '1=1',
90
+ xName = {},
91
+ metric,
92
+ } = normalizeData(data, query);
93
+
94
+ // get sql
95
+ const columns = data.columns?.map((el) => el.name || el)?.join(',') || '1';
96
+ const filterQ = filter
97
+ ? await getFilterSQL({ pg, table, filter })
98
+ : undefined;
99
+ const q = `select "${pkey}",
100
+ ${data?.color ? `"${data?.color}"` : '0'} as x,
101
+ ${data.metrics?.[0] ? `"${data.metrics[0]}"::float` : '0'} as metric,
102
+ ${columns},
103
+ ${geom} as geom
104
+ from ${filterQ ? `(${filterQ})` : table} q where ${where}`;
105
+
106
+ if (sql === '1') return q;
107
+
108
+ const koef =
109
+ {
110
+ 10: 0.1,
111
+ 11: 0.05,
112
+ 12: 0.005,
113
+ 13: 0.0002,
114
+ 14: 0.00005,
115
+ }[z] || 0.000001;
116
+
117
+ const geomCol =
118
+ parseInt(z, 10) < parseInt(pointZoom, 10) || true
119
+ ? `ST_Centroid(${geom})`
120
+ : geom;
121
+
122
+ const bbox = mercator.bbox(+y, +x, +z, false /* , '900913' */);
123
+ const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;
124
+
125
+ const areaZoom =
126
+ area[z] && false
127
+ ? ` and (st_area(st_transform(${geom},3857)))>'${area[z]}'`
128
+ : '';
129
+
130
+ const q1 =
131
+ cluster > z
132
+ ? `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
133
+ FROM (
134
+ SELECT floor(random() * 100000 + 1)::int + row_number() over() as row, count(*) as point_count,
135
+ ST_AsMVTGeom(st_transform(st_centroid(ST_Union(geom)),3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
136
+ FROM (
137
+ SELECT geom, ST_ClusterDBSCAN(geom,${koef},1) OVER () AS cluster
138
+ FROM (${q})q where ${geom} && ${bbox2d} ) j
139
+ WHERE cluster IS NOT NULL
140
+ GROUP BY cluster
141
+ ORDER BY 1 DESC
142
+ )q`
143
+ : `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
144
+ FROM (
145
+ SELECT
146
+ floor(random() * 100000 + 1)::int + row_number() over() as row,
147
+
148
+ ${pkey} as id,
149
+ x,
150
+ metric,
151
+ ${columns},
152
+ ST_AsMVTGeom(st_transform(${geomCol}, 3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
153
+
154
+ FROM (select * from (${q})q where ${geom} && ${bbox2d}
155
+
156
+ and ${geom} is not null and st_srid(${geom}) >0
157
+
158
+ ${areaZoom}
159
+
160
+ ${types[type] ? ` and ST_GeometryType(geom) = any ('{ ${types[type]} }') ` : ''}
161
+
162
+ limit 3000)q
163
+ ) q`;
164
+
165
+ if (sql === '2') return q1;
166
+
167
+ const { rows = [] } = await pg.query(q1);
168
+
169
+ if (sql === '3') return rows.map((el) => el.tile);
170
+
171
+ const buffer = Buffer.concat(rows.map((el) => Buffer.from(el.tile)));
172
+
173
+ if (!nocache) {
174
+ await mkdir(path.dirname(file), { recursive: true });
175
+ await writeFile(file, buffer, 'binary');
176
+ }
177
+
178
+ return reply.headers(headers).send(buffer);
179
+ } catch (err) {
180
+ logger.file('bi/vtile', { level: 'ERROR', error: err.toString(), query, params });
181
+ return { error: err.toString(), status: 500 };
182
+ }
183
+ }
@@ -0,0 +1,32 @@
1
+ import map from './controllers/map.js';
2
+ import geojson from './controllers/geojson.js';
3
+ import vtile from './controllers/vtile.js';
4
+
5
+ import cluster from './controllers/cluster.js';
6
+ import clusterVtile from './controllers/clusterVtile.js';
7
+ import heatmap from './controllers/heatmap.js';
8
+
9
+ const biSchema = {
10
+ type: 'object',
11
+ properties: {
12
+ querystring: {
13
+ widget: { type: 'string', pattern: '^([\\d\\w]+)$' },
14
+ dashboard: { type: 'string', pattern: '^([\\d\\w]+)$' },
15
+ sql: { type: 'string', pattern: '^([\\d])$' },
16
+ },
17
+ params: {
18
+ id: { type: 'string', pattern: '^([\\d\\w]+)$' },
19
+ },
20
+ },
21
+ };
22
+
23
+ const policy = ['public'];
24
+
25
+ export default async function route(fastify, opts) {
26
+ fastify.get('/bi-map', { config: { policy }, schema: biSchema }, map);
27
+ fastify.get('/bi-geojson', { config: { policy }, schema: biSchema }, geojson);
28
+ fastify.get('/bi-vtile/:z/:y/:x', { config: { policy }, schema: biSchema }, vtile);
29
+ fastify.get('/bi-cluster', { config: { policy }, schema: biSchema }, cluster);
30
+ fastify.get('/bi-cluster-vtile/:z/:y/:x', { config: { policy }, schema: biSchema }, clusterVtile);
31
+ fastify.get('/bi-heatmap', { config: { policy } }, heatmap);
32
+ }
@@ -0,0 +1,59 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="">
3
+
4
+ <head>
5
+ <!-- scripts -->
6
+ <script src="https://cdn.tailwindcss.com"></script>
7
+ </head>
8
+
9
+ <body class="bg-[#f3f4f6] flex items-center flex-1 w-full h-[100vh] overflow-x-hidden min-h-full ">
10
+ <main class="w-full">
11
+ <div class="flex w-full max-w-sm mx-auto overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800 lg:max-w-4xl">
12
+ <div class="hidden bg-left bg-cover lg:block lg:w-1/2" style="background-image: url('https://media.istockphoto.com/id/1296381479/vector/set-of-simple-infographic-graphs-and-charts-data-visualization-statistics-and-business.jpg?s=612x612&w=0&k=20&c=JpEoP-6w16OwVC49Sq2PEI8-dHF1CZnm7rHyL5DoxqA=');"></div>
13
+
14
+ <div class="w-full px-6 py-8 md:px-8 lg:w-1/2">
15
+ <div class="flex justify-center mx-auto">
16
+ <img width="300" height="100" src="https://cdn.softpro.ua/data/icons/logo-softpro.png" alt="logo">
17
+ </div>
18
+
19
+ <p class="mt-3 mb-[32px] text-xl text-center text-gray-600 dark:text-gray-200">
20
+ Аналітичний модуль
21
+ </p>
22
+
23
+ <form action="/api/login" method="get" >
24
+ <div class="mt-4">
25
+ <label class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-200" for="LoggingEmailAddress">Електронна пошта</label>
26
+ <input name="username" id="LoggingEmailAddress" class="block w-full px-4 py-2 text-gray-700 bg-white border rounded-lg dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring focus:ring-blue-300" type="text">
27
+ </div>
28
+
29
+ <div class="mt-4">
30
+ <div class="flex justify-between">
31
+ <label class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-200" for="loggingPassword">Пароль</label>
32
+ <!-- <a class="text-xs text-gray-500 dark:text-gray-300 hover:underline">Forget Password?</a> -->
33
+ </div>
34
+
35
+ <input id="loggingPassword" name="password" class="block w-full px-4 py-2 text-gray-700 bg-white border rounded-lg dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring focus:ring-blue-300" type="password">
36
+ </div>
37
+
38
+ <div class="mt-6">
39
+ <button type="submit" class="w-full px-6 py-3 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-gray-800 rounded-lg hover:bg-gray-700 focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50">
40
+ Увійти
41
+ </button>
42
+ </div>
43
+
44
+ </form>
45
+ </div>
46
+ </div>
47
+ </main>
48
+
49
+ </body>
50
+
51
+
52
+ <script>
53
+ const error = location.search.includes('wrong_pass')
54
+ if (error) {
55
+ document.getElementById('login_error').innerHTML = 'Не вірний користувач або пароль';
56
+ }
57
+ </script>
58
+
59
+ </html>
@@ -0,0 +1,117 @@
1
+ import { getTemplate, getPGAsync, pgClients, getMeta } from '@opengis/fastify-table/utils.js';
2
+
3
+ import mdToHTML from '../helpers/mdToHTML.js';
4
+
5
+ async function getWidget({ pg: pg1 = pgClients.client, dashboard, widget }) {
6
+ if (!dashboard && !widget) {
7
+ return { message: `not enough params: dashboard / widget`, status: 400 };
8
+ }
9
+ const dashboardData = dashboard
10
+ ? await getTemplate('dashboard', dashboard)
11
+ : null;
12
+
13
+ const dashboardData1 = await pg1.query(`select db, table_name, widgets as "dashboardWidgets" from bi.dashboard where dashboard_id=$1`, [dashboard])
14
+ .then(el => el.rows?.[0] || {});
15
+ const { dashboardWidgets = [] } = dashboardData1;
16
+ const dashboardDb = typeof dashboardData1?.db === 'string' && dashboardData1?.db?.startsWith('{') ? JSON.parse(dashboardData1?.db) : dashboardData1?.db;
17
+
18
+ const dashboardIndex = dashboardData?.find((el) => el[0] == 'index.yml')?.[1]; // dashboardData?.find((el) => el[2] == 'index')?.[1]
19
+ const pg = dashboardDb || dashboardIndex?.db ? await getPGAsync(dashboardDb || dashboardIndex?.db) : pg1;
20
+
21
+ const { id, tableName } =
22
+ !dashboardData && !dashboardData1 && pg.pk['bi.dashboard'] && dashboard
23
+ ? await pg1.query(`select dashboard_id as id, table_name as "tableName" from bi.dashboard where $1 in (dashboard_id, name)`, [dashboard])
24
+ .then(el => el.rows?.[0] || {})
25
+ : {};
26
+
27
+ if (!dashboardData && !dashboardData1 && dashboard && !id) {
28
+ return { message: `dashboard not found: ${dashboard}`, status: 404 };
29
+ }
30
+
31
+ dashboardData?.forEach((el) => {
32
+ el[2] = el[0].split('.')[0];
33
+ });
34
+
35
+ const widgetData = dashboard
36
+ ? dashboardData?.find((el) => el[2] === (widget || 'index'))?.[1]
37
+ || dashboardWidgets?.find(el => el.name === widget)
38
+ : await getTemplate('widget', widget);
39
+
40
+ if (typeof widgetData === 'string' || typeof widgetData?.html === 'string' || (widgetData?.type === 'text' && typeof widgetData?.data?.text === 'string')) {
41
+ const html = widgetData?.html || mdToHTML(widgetData?.data?.text || widgetData);
42
+ return { source: html, status: 200 };
43
+ }
44
+
45
+ if (!id && !dashboardData && !dashboardData1 && !widgetData) {
46
+ return { message: `not found ${widget} ${dashboard}`, status: 404 };
47
+ }
48
+
49
+ const q = `select *, title as text, coalesce(data::jsonb, '{}'::jsonb) || jsonb_build_object('table', table_name) as data from bi.widget where dashboard_id=$1 and name=$2`;
50
+
51
+ const {
52
+ type,
53
+ text,
54
+ data = {},
55
+ controls,
56
+ style,
57
+ options,
58
+ yml
59
+ } = widgetData ||
60
+ (await pg
61
+ .query(q, [id || dashboard, widget])
62
+ .then(el => el.rows?.[0] || {}));
63
+
64
+ if (!type) {
65
+ return { message: `widget not found: ${widget}`, status: 404 };
66
+ }
67
+
68
+ Object.assign(data, {
69
+ table:
70
+ data.table ||
71
+ tableName ||
72
+ widgetData?.table_name ||
73
+ dashboardIndex?.table ||
74
+ dashboardIndex?.table_name ||
75
+ dashboardData1?.table_name,
76
+ db: dashboardDb?.db || dashboardDb || dashboardIndex?.db || widgetData?.db || pg?.options?.database,
77
+ });
78
+ const main = { ...(dashboardIndex || {}), ...widgetData, ...data, ...data?.data || {} };
79
+ const widgetDb = widgetData?.db ? await getPGAsync(widgetData?.db) : pg;
80
+
81
+ if (!main?.table) {
82
+ return {
83
+ message: /* json.error || */ `invalid ${widget ? 'widget' : 'dashboard'}: empty table`,
84
+ status: 404,
85
+ };
86
+ }
87
+
88
+ const loadTemplate = pg.pk?.['admin.doc_template'] ? await pg.query(
89
+ 'select body from admin.doc_template where doc_type=5 and title=$1',
90
+ [main.table]
91
+ ).then(el => el.rows?.[0]?.body) : null;
92
+
93
+ const { pk, view, columns = [] } = await getMeta({ pg: widgetDb, table: loadTemplate?.table || main?.table });
94
+ if (!pk && !view) {
95
+ return {
96
+ message: /* json.error || */ `invalid ${widget ? 'widget' : 'dashboard'}: table not found (${loadTemplate?.table || main?.table})`,
97
+ status: 404,
98
+ };
99
+ }
100
+
101
+ const tableSQL = Array.isArray(main?.tableSQL) ? main?.tableSQL?.map?.(
102
+ (el, i) => `left join lateral(${el})t${i + 1} on 1=1`
103
+ )?.join?.(' ') : main?.tableSQL;
104
+
105
+ const columnList = columns.map(col => col.name);
106
+
107
+ const res = { ...main, pg: widgetDb, sql: widgetData?.sql, limit: widgetData?.limit, tableSQL, data, type, text, controls, style, options, yml };
108
+ if (res.x && !columnList.includes(res.x) && !loadTemplate?.table && !tableSQL?.includes?.(res.x)) {
109
+ Object.assign(res, { x: null, error: `column does not exists: ${res.x} at table ${res.table}` });
110
+ }
111
+ if (loadTemplate?.table) {
112
+ Object.assign(res.data || {}, { table: loadTemplate?.table });
113
+ Object.assign(res, { table: loadTemplate?.table, table_name: loadTemplate?.table, tableSQL: loadTemplate.sql?.filter?.(el => !el.disabled && el?.sql?.replace)?.map?.((el, i) => `left join lateral(${el.sql})t${i + 1} on 1=1`)?.join?.(' ') });
114
+ }
115
+ return res;
116
+ }
117
+ export default getWidget;
package/utils.js ADDED
@@ -0,0 +1,12 @@
1
+ // This file contains code that we reuse
2
+ // between our tests.
3
+
4
+ // import getTemplatePath from '@opengis/fastify-table/table/controllers/utils/getTemplatePath.js';
5
+ import getWidget from './server/utils/getWidget.js';
6
+ import yamlSafe from './server/routes/dashboard/controllers/utils/yaml.js';
7
+
8
+ export {
9
+ // getTemplatePath,
10
+ yamlSafe,
11
+ getWidget,
12
+ };