@opengis/bi 1.0.11 → 1.0.13

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 (76) hide show
  1. package/dist/assets/charts/bar.png +0 -0
  2. package/dist/assets/charts/funnel.png +0 -0
  3. package/dist/assets/charts/no_data.jpg +0 -0
  4. package/dist/assets/charts/number.png +0 -0
  5. package/dist/assets/charts/pie.png +0 -0
  6. package/dist/assets/charts/progress.png +0 -0
  7. package/dist/assets/charts/stat.png +0 -0
  8. package/dist/assets/images/bar.png +0 -0
  9. package/dist/assets/images/funnel.png +0 -0
  10. package/dist/assets/images/no_data.jpg +0 -0
  11. package/dist/assets/images/number.png +0 -0
  12. package/dist/assets/images/pie.png +0 -0
  13. package/dist/assets/images/progress.png +0 -0
  14. package/dist/assets/images/stat.png +0 -0
  15. package/dist/bi.js +1 -1
  16. package/dist/bi.umd.cjs +722 -113
  17. package/dist/{import-file-D6RYWvi_.js → import-file-1T7kpSzt.js} +43137 -43183
  18. package/dist/map-component-mixin-BLM9iEWA.js +18712 -0
  19. package/dist/style.css +1 -1
  20. package/dist/vs-calendar-WiK1hcHS.js +96 -0
  21. package/dist/vs-funnel-bar-CpPbYZ0_.js +92 -0
  22. package/dist/vs-heatmap-BG4eIROH.js +83 -0
  23. package/dist/vs-map-BRk6Fmks.js +66 -0
  24. package/dist/vs-map-cluster-Dfe9INqE.js +103 -0
  25. package/dist/vs-number-CJq-vi95.js +39 -0
  26. package/dist/{vs-text-UyIWGqQO.js → vs-text-DcrAdQ40.js} +60 -60
  27. package/package.json +11 -6
  28. package/server/migrations/bi.sql +27 -0
  29. package/server/plugins/hook.js +86 -0
  30. package/server/plugins/vite.js +7 -4
  31. package/server/routes/dashboard/controllers/dashboard.js +24 -4
  32. package/server/routes/data/controllers/data.js +48 -52
  33. package/server/routes/data/controllers/util/chartSQL.js +8 -7
  34. package/server/routes/data/controllers/util/normalizeData.js +22 -9
  35. package/server/routes/edit/controllers/dashboard.add.js +5 -1
  36. package/server/routes/edit/controllers/dashboard.edit.js +5 -1
  37. package/server/routes/edit/controllers/widget.add.js +23 -7
  38. package/server/routes/edit/controllers/widget.del.js +1 -1
  39. package/server/routes/edit/controllers/widget.edit.js +8 -5
  40. package/server/routes/map/controllers/cluster.js +75 -0
  41. package/server/routes/map/controllers/clusterVtile.js +143 -0
  42. package/server/routes/map/controllers/geojson.js +9 -23
  43. package/server/routes/map/controllers/map.js +57 -0
  44. package/server/routes/map/controllers/vtile.js +25 -32
  45. package/server/routes/map/index.mjs +7 -4
  46. package/server/utils/getWidget.js +56 -0
  47. package/utils.js +11 -0
  48. package/dist/vs-number-DKF5ptAP.js +0 -34
  49. package/server/templates/dashboard/erobota/bar_area.yml +0 -17
  50. package/server/templates/dashboard/erobota/bar_culture.yml +0 -18
  51. package/server/templates/dashboard/erobota/bar_grand.yml +0 -18
  52. package/server/templates/dashboard/erobota/count_grand.yml +0 -8
  53. package/server/templates/dashboard/erobota/index.yml +0 -47
  54. package/server/templates/dashboard/erobota/list_culture.yml +0 -12
  55. package/server/templates/dashboard/erobota/list_grant.yml +0 -12
  56. package/server/templates/dashboard/erobota/map.yml +0 -4
  57. package/server/templates/dashboard/erobota/pie_area.yml +0 -17
  58. package/server/templates/dashboard/erobota/pie_grant.yml +0 -17
  59. package/server/templates/dashboard/erobota/total_area.yml +0 -9
  60. package/server/templates/dashboard/erobota/total_grand.yml +0 -9
  61. package/server/templates/dashboard/sales/index.yml +0 -40
  62. package/server/templates/dashboard/sales/quarterly_revenue.yml +0 -19
  63. package/server/templates/dashboard/sales/quarterly_revenue_by_product_line.yml +0 -19
  64. package/server/templates/dashboard/sales/total_products_sold.yml +0 -9
  65. package/server/templates/dashboard/sales/total_products_sold_by_product_line.yml +0 -12
  66. package/server/templates/dashboard/sales/total_revenue.yml +0 -8
  67. package/server/templates/dashboard/sales/total_revenue_by_product_line.yml +0 -17
  68. package/server/templates/dashboard/sales/vehicle_sales_info.md +0 -17
  69. package/server/templates/dashboard/test3/index.yml +0 -29
  70. package/server/templates/dashboard/test3/quarterly_revenue.yml +0 -19
  71. package/server/templates/dashboard/test3/widget1.yml +0 -8
  72. package/server/templates/pt/vehicle_sales.md +0 -17
  73. package/server/templates/table/demo.cleaned_sales_data.table.json +0 -104
  74. package/server/templates/table/test.dataset.table.json +0 -16
  75. package/server/templates/widget/product_line.yml +0 -20
  76. package/server/templates/widget/test_vtile.yml +0 -7
@@ -1,98 +1,94 @@
1
1
 
2
- // import getTemplate from '@opengis/fastify-table/table/controllers/utils/getTemplate.js';
3
- import autoIndex from '@opengis/fastify-table/pg/funcs/autoIndex.js';
4
- import pgClients from '@opengis/fastify-table/pg/pgClients.js';
2
+
3
+ //import autoIndex from '@opengis/fastify-table/pg/funcs/autoIndex.js';
4
+ // import pgClients from '@opengis/fastify-table/pg/pgClients.js';
5
+
6
+ import { getPGAsync, autoIndex, pgClients, getSelectVal } from '@opengis/fastify-table/utils.js';
5
7
  import chartSQL from './util/chartSQL.js';
6
8
  import normalizeData from './util/normalizeData.js';
7
9
 
8
-
9
- import { getTemplate } from '@opengis/fastify-table/utils.js';
10
+ import { getWidget } from '../../../../utils.js';
10
11
 
11
12
  export default async function data({
12
- pg = pgClients.client, funcs = {}, query = {},
13
+ funcs = {}, query = {},
13
14
  }) {
14
15
  const time = Date.now();
15
- const { config = {} } = funcs;
16
- const { dashboard, widget } = query;
17
-
18
- if (!widget && !dashboard) {
19
- return { message: 'not enough params: widget or dashboard required', status: 400 };
20
- }
21
- const dashboardData = dashboard ? await getTemplate('dashboard', dashboard) : null;
22
-
23
- const { id, tableName } = !dashboardData && pg.pk['bi.dashboard'] && dashboard ?
24
- await pg.query(`select dashboard_id as id, table_name as "tableName" from bi.dashboard where $1 in (dashboard_id, name)`, [dashboard]).then((res1) => res1.rows?.[0] || {}) : {};
25
-
26
-
27
- if (!dashboardData && dashboard && !id) return 'dashboard not found';
28
-
29
- // return { t: 111, data: dashboardData, dashboard }
30
-
31
- const widgetData = dashboard ? dashboardData?.find(el => el[0]?.includes(widget))?.[1] : await getTemplate('widget', widget);
16
+ const { dashboard, widget, filter, search, samples } = query;
32
17
 
18
+ const widgetData = await getWidget({ dashboard, widget });
33
19
 
34
- if (dashboardData && !widgetData) {
35
- return widgetData
36
- }
37
-
38
- if (!id && !dashboardData && !widgetData) {
39
- return { message: { root: config?.local ? dashboardDir : undefined, error: `not found`, widget, dashboard }, status: 404 };
40
- }
20
+ if (widgetData.status) return widgetData;
41
21
 
42
- const q = `select *, coalesce(data::jsonb, '{}'::jsonb) || jsonb_build_object('table', table_name) as data from bi.widget where dashboard_id=$1 and name=$2`;
43
- const { type, text, data, controls, style, options } = widgetData || await pg.query(q, [id || dashboard, widget]).then((res1) => res1.rows?.[0] || {});
44
-
45
- if (typeof widgetData === 'string') {
46
- return { title: false, source: widgetData };
47
- }
22
+ const { type, text, data = {}, controls, style, options } = widgetData;
48
23
 
49
- if (!data?.table && tableName) {
50
- Object.assign(data, { table: tableName });
51
- }
24
+ const pg = data.db ? await getPGAsync(data.db) : pgClients.client
52
25
 
53
- if (!data?.table) {
54
- return { error: /* json.error || */ `invalid ${widget ? 'widget' : 'dashboard'}: 1`, status: 500 };
55
- }
26
+ const { fields: cols } = await pg.query(`select * from ${data.table} limit 0`);
27
+ const columnTypes = cols.map(el => ({ name: el.name, type: pg.pgType?.[el.dataTypeID] }))
56
28
 
57
29
  // data param
58
- const { x, metric, table, where, groupby, xName } = normalizeData(data, query);
30
+ const { x, cls, metric, table, where, tableSQL, groupby, xName } = normalizeData(widgetData, query, columnTypes);
59
31
 
60
32
  // auto Index
61
- if (pg.pk[data.table]) {
62
- autoIndex({ table: data.table, columns: [data?.time].concat([xName]).concat([groupby]).filter(el => el) }).catch(err => console.log(err))
33
+ if (pg.pk?.[data.table]) {
34
+ autoIndex({ table: data.table, pg, columns: [data?.time].concat([xName]).concat([groupby]).filter(el => el) }).catch(err => console.log(err))
63
35
  }
64
36
 
65
37
  // get group
66
- const groupData = groupby ? await pg.query(`select ${groupby} as name ,count(*) from ${table} group by ${groupby} order by count(*) desc limit 20`).then(el => el.rows) : null;
38
+ const groupData = groupby ? await pg.query(`select ${groupby} as name ,count(*) from ${tableSQL || table} group by ${groupby} order by count(*) desc limit 20`).then(el => el.rows) : null;
39
+
40
+ if (query.sql === '2') return { x, metric, table, tableSQL, data, groupData };
67
41
 
68
- if (query.sql === '2') return { x, metric, table, data, groupData };
42
+ const order = data.order || (type === 'listbar' ? 'metric desc' : null);
69
43
 
70
- const sql = (chartSQL[type] || chartSQL['chart'])({ where, metric, table, x, groupData, groupby });
44
+ const { optimizedSQL = `select * from ${tableSQL || table}` } = filter || search ? await funcs.getFilterSQL({
45
+ pg, table, filter, search,
46
+ }) : {};
71
47
 
48
+ const sql = (chartSQL[type] || chartSQL['chart'])({ where, metric, table: `(${optimizedSQL})q`, x, groupData, groupby, order, samples });
72
49
 
73
50
  if (query.sql) return sql;
74
51
 
75
52
  if (!sql || sql?.includes('undefined')) {
76
- return { message: { error: 'invalid sql', type, sql, where, metric, table, x, groupData, groupby }, status: 500 };
53
+ return { message: { error: 'invalid sql', type, sql, where, metric, table: `(${optimizedSQL})q`, x, groupData, groupby }, status: 500 };
77
54
  }
78
55
 
79
56
  const { rows, fields } = await pg.query(sql); // test with limit
80
57
 
81
- const dimensions = fields.map(el => el.name)
58
+ if (cls) {
59
+ const values = rows.map((row) => row[x])?.filter((el, idx, arr) => el && arr.indexOf(el) === idx);
60
+ const vals = await getSelectVal({ pg, name: cls, values });
61
+ rows.filter((row) => row[x]).forEach((row) => {
62
+ Object.assign(row, { [x]: vals?.[row[x]] || row[x] });
63
+ });
64
+ }
65
+
66
+ const dimensions = fields.map(el => el.name);
82
67
 
83
68
  const res = {
84
69
  time: Date.now() - time,
85
70
  dimensions,
86
71
 
87
- dimensionsType: fields.map(el => pg.pgType[el.dataTypeID]),
72
+ dimensionsType: fields.map(el => pg.pgType?.[el.dataTypeID]),
88
73
  type,
89
74
 
90
- text: text ? text : data.text,
75
+ text: text ? text : (widgetData?.title || data.text),
91
76
  //data: query.format === 'data' ? dimensions.map(el => rows.map(r => r[el])) : undefined,
92
77
  source: query.format === 'array' ? dimensions.map(el => rows.map(r => r[el])) : rows,
93
78
  style,
94
79
  options,
95
80
  controls,
81
+ params: {
82
+ x,
83
+ cls,
84
+ metric,
85
+ table,
86
+ tableSQL,
87
+ where,
88
+ groupby,
89
+ sql,
90
+ },
91
+ columnTypes,
96
92
  };
97
93
  return res;
98
94
 
@@ -1,19 +1,20 @@
1
- function number({ metric, where, table }) {
2
- const sql = `select ${metric} from ${table} where ${where}`
1
+ function number({ metric, where, table, samples }) {
2
+ const sql = `select ${metric} from ${table} where ${where} ${samples ? 'limit 10' : ''}`
3
3
  return sql;
4
4
  }
5
- function table({ columns, table, where }) {
6
- return `select ${columns.map(el => el.name || el)}::text from ${table} where ${where} limit 20 `
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
7
  }
8
8
 
9
- function chart({ metric, where, table, x, groupby, groupData }) {
9
+ function chart({ metric, where, table, x, groupby, groupData, order, samples }) {
10
10
 
11
- const metricData = groupData?.map(el => `${metric} filter (where ${groupby}='${el.name}') as "${el.name}"`).join(',') || metric
11
+ const metricData = groupData?.map(el => `${metric} filter (where ${groupby}='${el.name}') as "${el.name}"`).join(',') || `${metric} as metric`
12
12
  const sql = `select ${x}, ${metricData}
13
13
  from ${table}
14
14
  where ${where}
15
15
  group by ${x}
16
- order by ${x}`;
16
+ order by ${order || x}
17
+ ${samples ? 'limit 10' : 'limit 100'}`;
17
18
  return sql;
18
19
  }
19
20
 
@@ -1,21 +1,34 @@
1
- function normalizeData(data, query = {}) {
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])) { delete query[el]; }
5
+ });
2
6
 
7
+ if (!columnTypes.find(col => col.type === 'numeric' && col.name == query.metric)) { delete query.metric; }
3
8
 
4
- const granularity = query.granularity || data.granularity;
9
+ const xName = query.x || (Array.isArray(data.x) ? data.x[0] : data.x);
10
+ const xTYpe = columnTypes.find(el => el.name == xName)?.type;
5
11
 
12
+ const granularity = xTYpe === 'date' || xTYpe?.includes('timestamp') ? (query.granularity || data.granularity || 'year') : null;
6
13
 
7
- const xName = Array.isArray(data.x) ? data.x[0] : data.x || query.x;
8
-
9
- const x = (granularity ? `date_trunc('${granularity}',${xName})::date` : null) || xName;
14
+ const x = (granularity ? `date_trunc('${granularity}',${xName})::date::text` : null) || xName;
10
15
 
11
16
  const metrics = Array.isArray(data.metrics) ? data.metrics : [data.metrics];
12
- const metric = metrics.length ? metrics?.filter(el => el).map(el => `${el.operator || 'sum'}(${el.name || el})`) : 'count(*)';
17
+ const metric = (query.metric ? `sum(${query.metric})` : null) || (metrics.length ? metrics?.filter(el => el).map(el => el.fx || `${el.operator || 'sum'}(${el.name || el})`) : 'count(*)');
13
18
 
14
- const table = data.table;
15
- const where = data.query || 'true';
19
+ const { cls, table, filterCustom } = data;
16
20
  const groupby = query.groupby || data.groupby;
17
21
  //const orderby = query.orderby || data.orderby || 'count(*)';
18
22
 
19
- return { x, metric, table, where, groupby, xName }
23
+ const custom = query?.filterCustom?.split(',')
24
+ ?.map((el) => filterCustom?.find((item) => item?.name === el)?.sql)
25
+ ?.filter((el) => el)?.join(' and ');
26
+ const where = `${data.query || '1=1'} and ${custom || 'true'}`;
27
+
28
+ const tableSQL = data.tableSQL?.length
29
+ ? `(select * from ${data?.table} t ${data.tableSQL.join(' \n ')} where ${where})q`
30
+ : undefined;
31
+
32
+ return { x, cls, metric, table, where, tableSQL, groupby, xName }
20
33
  }
21
34
  export default normalizeData;
@@ -1,7 +1,11 @@
1
1
  export default async function widgetAdd({ pg, funcs, params = {}, body }) {
2
2
  try {
3
3
  const time = Date.now();
4
- console.log(body);
4
+ const tableName = body.table_name;
5
+ const checkTable = await pg.query(`select * from bi.dashboard where $1 in (table_name)`, [
6
+ tableName
7
+ ]);
8
+ if (!checkTable.rows.length) return { message: 'bad params', status: 401 };
5
9
  const res = await funcs.dataInsert({
6
10
  table: 'bi.dashboard',
7
11
  data: body
@@ -8,7 +8,11 @@ export default async function dashboardEdit({
8
8
  status: 400,
9
9
  }
10
10
  }
11
-
11
+ const tableName = body.table_name;
12
+ const checkTable = await pg.query(`select * from bi.dashboard where $1 in (table_name)`, [
13
+ tableName
14
+ ]);
15
+ if (!checkTable.rows.length) return { message: 'bad params', status: 401 };
12
16
  const { name: dashboardName } = params;
13
17
  const row = await pg.query(`select dashboard_id from bi.dashboard where $1 in (dashboard_id, name)`, [dashboardName]).then((res1) => res1.rows?.[0] || {});
14
18
  const { dashboard_id: dashboardId } = row;
@@ -1,15 +1,26 @@
1
- export default async function widgetAdd({
2
- pg, funcs, params = {}, body = {},
3
- }) {
1
+ export default async function widgetAdd({ pg, funcs, params = {}, body = {} }) {
4
2
  const { name: dashboardName } = params;
5
3
  if (!dashboardName) {
6
4
  return { message: 'not enough params: id', status: 400 };
7
5
  }
8
-
6
+ const tableName = body.table_name;
9
7
  try {
10
- const row = await pg.query(`select dashboard_id, widgets, panels from bi.dashboard where $1 in (dashboard_id,name)`, [dashboardName]).then((res) => res.rows?.[0] || {});
8
+ const row = await pg
9
+ .query(
10
+ `select dashboard_id, widgets, panels from bi.dashboard where $1 in (dashboard_id,name)`,
11
+ [dashboardName]
12
+ )
13
+ .then((res) => res.rows?.[0] || {});
14
+
15
+ const checkTable = await pg.query(`select * from bi.widget where $1 in (table_name)`, [
16
+ tableName
17
+ ]);
18
+ if (!checkTable.rows.length) return { message: 'bad params', status: 401 };
19
+
11
20
  const { dashboard_id: dashboardId } = row;
12
21
 
22
+ const generatedName = generateUniqueName(body.type);
23
+ body.name = generatedName;
13
24
  const res = await funcs.dataUpdate({
14
25
  table: 'bi.dashboard',
15
26
  id: dashboardId,
@@ -20,14 +31,19 @@ export default async function widgetAdd({
20
31
  });
21
32
  const res2 = await funcs.dataInsert({
22
33
  table: 'bi.widget',
23
- data: { ...body, data: body, dashboard_id: dashboardId },
34
+ data: { ...body, data: body, dashboard_id: dashboardId }
24
35
  });
25
36
  return {
26
37
  message: `Added widget to ${dashboardName}`,
27
38
  status: 200,
28
- rows: res,
39
+ rows: res
29
40
  };
30
41
  } catch (err) {
31
42
  return { error: err.toString(), status: 500 };
32
43
  }
33
44
  }
45
+ function generateUniqueName(prefix = 'bar') {
46
+ const randomPart = Math.floor(Math.random() * 10000);
47
+ const timestamp = Date.now();
48
+ return `${prefix}_${randomPart}_${timestamp}`;
49
+ }
@@ -14,7 +14,7 @@ export default async function widgetDel({ pg = {}, params = {} }) {
14
14
  const row = await pg
15
15
  .query(
16
16
  `select a.widget_id, b.dashboard_id from bi.widget a, bi.dashboard b
17
- where a.name=$2 and $1 in (b.dashboard_id, b.name) order by 1,2`,
17
+ where $2 in (a.widget_id, a.name) and $1 in (b.dashboard_id, b.name) order by 1,2`,
18
18
  [dashboardName, widgetName]
19
19
  )
20
20
  .then((res1) => res1.rows?.[0] || {});
@@ -1,7 +1,7 @@
1
1
  import dataUpdate from '@opengis/fastify-table/crud/funcs/dataUpdate.js';
2
2
  export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, params = {} }) {
3
3
  const { widget: widgetName, name: dashboardName } = params;
4
-
4
+ const tableName = body.table_name;
5
5
  if (!widgetName || !dashboardName) {
6
6
  return {
7
7
  message: 'not enough params: dashboard and widget name',
@@ -13,12 +13,15 @@ export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, param
13
13
  const row = await pg
14
14
  .query(
15
15
  `select a.widget_id, b.dashboard_id from bi.widget a, bi.dashboard b
16
- where a.name=$2 and $1 in (b.dashboard_id, b.name) order by 1,2`,
16
+ where $2 in (a.widget_id, a.name) and $1 in (b.dashboard_id, b.name) order by 1,2`,
17
17
  [dashboardName, widgetName]
18
18
  )
19
19
  .then((res1) => res1.rows?.[0] || {});
20
-
20
+ const checkTable = await pg.query(`select * from bi.widget where $1 in (table_name)`, [
21
+ tableName
22
+ ]);
21
23
  const { widget_id: widgetId, dashboard_id: dashboardId } = row || {};
24
+ if (!checkTable.rows.length) return { message: 'bad params', status: 401 };
22
25
 
23
26
  if (!widgetId) {
24
27
  return { message: `widget not found ${widgetName}`, status: 404 };
@@ -47,7 +50,7 @@ export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, param
47
50
  Array.isArray(bodyDashboard.panels) && bodyDashboard.panels?.length
48
51
  ? bodyDashboard.panels?.map((panel) => {
49
52
  if (panel.widget === widgetName) {
50
- panel.col = body.col;
53
+ panel.col = body.col;
51
54
  }
52
55
  return panel;
53
56
  })
@@ -56,7 +59,7 @@ export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, param
56
59
  Array.isArray(bodyDashboard.widgets) && bodyDashboard?.widgets?.length
57
60
  ? bodyDashboard.widgets?.map((widget) => {
58
61
  if (widget.name === widgetName) {
59
- return body;
62
+ return body;
60
63
  }
61
64
  return widget;
62
65
  })
@@ -0,0 +1,75 @@
1
+
2
+ import { getWidget } from '../../../../utils.js';
3
+
4
+ export default async function cluster({
5
+ pg, funcs, query = {}, log,
6
+ }) {
7
+ const {
8
+ widget, filter, dashboard, search,
9
+ } = query;
10
+
11
+ if (!widget) {
12
+ return { message: 'not enough params: widget', status: 400 };
13
+ }
14
+
15
+ const { data } = await getWidget({ dashboard, widget })
16
+
17
+ try {
18
+
19
+ const pkey = pg.pk?.[data?.table];
20
+
21
+ if (!pkey) {
22
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`, status: 400 };
23
+ }
24
+
25
+ // data param
26
+ const { table, query: where = '1=1', metrics = [], cluster, clusterTable = {} } = data;
27
+
28
+ if (!cluster) {
29
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`, status: 400 };
30
+ }
31
+
32
+ if (!metrics.length) {
33
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`, status: 400 };
34
+ }
35
+
36
+ if (!clusterTable?.name) {
37
+ return { message: 'not enough widget params: clusterTable name', status: 400 };
38
+ }
39
+
40
+ if (clusterTable?.name && !pg.pk?.[clusterTable?.name]) {
41
+ return { message: 'invalid widget params: clusterTable pkey not found', status: 404 };
42
+ }
43
+
44
+ const { total = 0 } = pg.pk?.[table]
45
+ ? await pg.queryCache(`select oid::regclass as table, reltuples AS total from pg_class`)
46
+ .then((res) => res.rows?.find((row) => row.table === table))
47
+ : {};
48
+
49
+ const { bbox } = await pg.query(`select st_asgeojson(box2d(geom))::json as bbox from ${table} where ${where || '1=1'}`).then((res1) => res1.rows?.[0] || {});
50
+
51
+ // get sql
52
+ const { optimizedSQL } = filter || search
53
+ ? await funcs.getFilterSQL({ pg, table, filter, search })
54
+ : {};
55
+
56
+ const q = `select "${cluster}" as name, sum("${metrics[0]}")::float as metric
57
+ from ${optimizedSQL ? `(${optimizedSQL})` : table} q
58
+ left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id from ${clusterTable?.name} where ${clusterTable?.title}=q."${cluster}" limit 1)b on 1=1
59
+ where ${where} group by ${cluster}, b.id`;
60
+
61
+ if (query.sql === '1') return q;
62
+
63
+ // auto Index
64
+ // funcs.autoIndex({ table, columns: (metrics || []).concat([cluster]) });
65
+
66
+ const { rows = [] } = await pg.query(q);
67
+ const vals = rows.map(el => el.metric - 0).sort((a, b) => a - b);
68
+ const len = vals.length;
69
+ const sizes = [vals[0], vals[Math.floor(len / 4)], vals[Math.floor(len / 2)], vals[Math.floor(len * 0.75)], vals[len - 1]]
70
+ return { sizes, rows, bbox, count: rows.length, total };
71
+ } catch (err) {
72
+ log.error('bi/cluster', { error: err.toString(), query });
73
+ return { error: err.toString(), status: 500 };
74
+ }
75
+ }
@@ -0,0 +1,143 @@
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
+
7
+ import { getWidget } from '../../../../utils.js';
8
+
9
+ const mercator = new Sphericalmercator({ size: 256 });
10
+
11
+ const clusterExists = {};
12
+
13
+ export default async function clusterVtile(req, reply) {
14
+ const {
15
+ pg, funcs, params = {}, query = {}, log,
16
+ } = req;
17
+ const { z, y } = params;
18
+ const x = params.x?.split('.')[0] - 0;
19
+
20
+ if (!x || !y || !z) {
21
+ return { message: 'not enough params: xyz', status: 400 };
22
+ }
23
+
24
+ const {
25
+ widget, filter, dashboard, search, clusterZoom, nocache, pointZoom,
26
+ } = query;
27
+
28
+ if (!widget) {
29
+ return { message: 'not enough params: widget', status: 400 };
30
+ }
31
+
32
+
33
+ const { data } = await getWidget({ dashboard, widget })
34
+
35
+
36
+ const headers = {
37
+ 'Content-Type': 'application/x-protobuf',
38
+ 'Cache-Control': nocache || query.sql
39
+ ? 'no-cache'
40
+ : 'public, max-age=86400',
41
+ };
42
+
43
+ const hash = [pointZoom, filter].filter((el) => el).join();
44
+
45
+ const root = funcs.getFolder(req);
46
+ const file = path.join(root, `/map/vtile/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}${z}/${x}/${y}.mvt`);
47
+
48
+ try {
49
+
50
+
51
+ if (!data?.table) {
52
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table not specified`, status: 400 };
53
+ }
54
+
55
+ const pkey = pg.pk?.[data?.table];
56
+
57
+ if (!pkey) {
58
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`, status: 400 };
59
+ }
60
+
61
+ // data param
62
+ const { table, query: where = '1=1', metrics = [], cluster, clusterTable = {} } = data;
63
+ if (!clusterTable?.name) {
64
+ Object.assign(data, { clusterTable: { name: 'bi.cluster', title, query: `type='${data.cluster}'` } });
65
+ }
66
+
67
+ if (!clusterExists[data?.cluster]) {
68
+ // const res = await fetch('https://cdn.softpro.ua/data/bi/region-ua.json');
69
+ // if (res?.status !== 200) {
70
+ // }
71
+ // await pg.query(`insert into bi.cluster (title,type,geom) values()`);
72
+ // clusterExists[data?.cluster] = 1;
73
+ }
74
+
75
+ if (!cluster) {
76
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`, status: 400 };
77
+ }
78
+
79
+ if (!metrics.length) {
80
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`, status: 400 };
81
+ }
82
+
83
+ // get sql
84
+ const { optimizedSQL } = filter || search
85
+ ? await funcs.getFilterSQL({ pg, table, filter, search })
86
+ : {};
87
+
88
+ const q = `select "${cluster}" as name, sum("${metrics[0]}")::float as metric, b.*
89
+ from ${optimizedSQL ? `(${optimizedSQL})` : table} q
90
+ left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id,
91
+ ${clusterTable?.geom || 'geom'} as geom from ${clusterTable?.name}
92
+ where ${clusterTable?.query || '1=1'} and ${clusterTable?.title}=q."${cluster}" limit 1
93
+ )b on 1=1
94
+ where ${where} group by
95
+ ${cluster}, b.id, b.${clusterTable?.geom || 'geom'}`;
96
+
97
+ if (query.sql === '1') return q;
98
+
99
+ const geomCol = parseInt(z, 10) < parseInt(pointZoom, 10) ? `ST_Centroid(${clusterTable?.geom || data?.geom || 'geom'})` : clusterTable?.geom || data?.geom || 'geom';
100
+
101
+ const bbox = mercator.bbox(+y, +x, +z, false/* , '900913' */);
102
+ const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;
103
+
104
+ const q1 = `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
105
+ FROM (
106
+ SELECT
107
+ floor(random() * 100000 + 1)::int + row_number() over() as row,
108
+
109
+ ${pg.pk?.[clusterTable?.name] ? 'id,' : ''} name, metric,
110
+
111
+ ST_AsMVTGeom(st_transform(${geomCol}, 3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
112
+
113
+ FROM (select * from (${q})q where geom && ${bbox2d}
114
+
115
+ and geom is not null and st_srid(geom) >0
116
+
117
+ and ST_GeometryType(geom) = any ('{ "ST_Polygon", "ST_MultiPolygon" }')
118
+
119
+ limit 3000)q
120
+ ) q`;
121
+
122
+ if (query.sql === '2') return q1;
123
+
124
+ // auto Index
125
+ funcs.autoIndex({ table, columns: (metrics || []).concat([cluster]) });
126
+
127
+ const { rows = [] } = await pg.query(q1);
128
+
129
+ if (query.sql === '3') return rows.map((el) => el.tile);
130
+
131
+ const buffer = Buffer.concat(rows.map((el) => Buffer.from(el.tile)));
132
+
133
+ if (!nocache) {
134
+ await mkdir(path.dirname(file), { recursive: true });
135
+ await writeFile(file, buffer, 'binary');
136
+ }
137
+
138
+ return reply.headers(headers).send(buffer);
139
+ } catch (err) {
140
+ log.error('bi/clusterVtile', { error: err.toString(), query, params });
141
+ return { error: err.toString(), status: 500 };
142
+ }
143
+ }
@@ -8,6 +8,8 @@ import { existsSync, /*readdirSync, */ readFileSync } from 'fs';
8
8
  import yaml from '../../dashboard/controllers/utils/yaml.js';
9
9
  import normalizeData from '../../data/controllers/util/normalizeData.js';
10
10
 
11
+ import { getWidget } from '../../../../utils.js';
12
+
11
13
  const types = { point: 'ST_Point' /* ,ST_MultiPoint */, polygon: 'ST_Polygon,ST_MultiPolygon' };
12
14
  const hourMs = 3.6e+6;
13
15
 
@@ -19,20 +21,12 @@ export default async function geojson(req, reply) {
19
21
 
20
22
  const { filter, widget, sql, type, nocache, id, dashboard, geom = 'geom', pointZoom = 0 } = query;
21
23
 
22
- const { config = {} } = funcs;
23
-
24
24
  if (!widget && !dashboard) {
25
25
  return { message: 'not enough params: widget', status: 400 };
26
26
  }
27
27
 
28
- const cwd = process.cwd();
29
- const widgetDir = dashboard ? path.join(cwd, 'server/templates/dashboard', dashboard) : path.join(cwd, 'server/templates/widget');
30
-
31
- const filePath = dashboard ? path.join(widgetDir, widget || 'index' + '.yml') : path.join(widgetDir, widget + '.yml');
32
-
33
- if (!existsSync(filePath)) {
34
- return { message: { root: config?.local ? widgetDir : undefined, error: `not found`, widget }, status: 404 };
35
- }
28
+ const data = await getWidget({ dashboard, widget });
29
+ if (data.status) return data;
36
30
 
37
31
  const hash = [pointZoom, filter].filter((el) => el).join();
38
32
 
@@ -50,29 +44,21 @@ export default async function geojson(req, reply) {
50
44
  }
51
45
 
52
46
  try {
53
- const fileData = readFileSync(filePath, 'utf-8');
54
- const { data = {}, table_name: tableName } = filePath.includes('.yml') ? yaml.loadSafe(fileData) : {};
55
- if (!data?.table && tableName) Object.assign(data, { table: tableName });
56
-
57
- if (!data?.table) {
58
- return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table not specified`, status: 400 };
59
- }
60
-
61
47
  const pkey = pg.pk?.[data?.table];
62
48
  if (!pkey) {
63
49
  return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`, status: 400 };
64
50
  }
65
51
 
66
52
  // data param
67
- const { table, where = '1=1', xName = {}, x } = normalizeData(data, query);
53
+ const { table, where = '1=1', xName, x } = normalizeData(data, query);
68
54
 
69
- if (!xName?.name && !x) {
55
+ if (!xName && !x) {
70
56
  return { message: `invalid ${widget ? 'widget' : 'dashboard'}: x axis column not specified`, status: 400 };
71
57
  }
72
58
 
73
59
  // get sql
74
60
  const filterQ = filter ? await funcs.getFilterSQL({ pg, table, filter, query }) : undefined;
75
- const q = `select "${pkey}", "${xName?.name || x}", /* st_asgeojson(geom)::json as */ ${geom} as geom from ${filterQ ? `(${filterQ})` : table} q where ${where}`;
61
+ const q = `select "${pkey}", "${xName || x}", /* st_asgeojson(geom)::json as */ ${geom} as geom from ${filterQ ? `(${filterQ})` : table} q where ${where}`;
76
62
 
77
63
  if (sql === '1') return q;
78
64
 
@@ -85,7 +71,7 @@ export default async function geojson(req, reply) {
85
71
  ? `st_transform(${type === 'centroid' ? `st_centroid(${geom})` : geom},${query.srid})`
86
72
  : `${type === 'centroid' || query.point || query.centroid ? `st_centroid(${geom})` : geom}`}), 6, 0)::json as geometry,
87
73
  (select row_to_json(tc) from (select ${'' ? `${''} as status, ` : ''}
88
- ${xName?.name ? `${xName?.name},` : ''}
74
+ ${xName ? `${xName},` : ''}
89
75
  ${data.style?.colorAttr ? `${data.style.colorAttr},` : ''}
90
76
  ${pkey} as id,(select file_path from crm.files
91
77
  where entity_id=q.${pkey}::text and file_status <>'3' and ext in ('png','jpg') limit 1) as image
@@ -99,7 +85,7 @@ export default async function geojson(req, reply) {
99
85
  if (sql === '2') return q1;
100
86
 
101
87
  // auto Index
102
- funcs.autoIndex({ table, columns: [xName.name] });
88
+ funcs.autoIndex({ table, columns: [xName] });
103
89
 
104
90
  const res = await pg.query(q1).then((res) => res.rows?.[0] || {});
105
91