@opengis/bi 1.0.37 → 1.0.38

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 (31) hide show
  1. package/config.js +12 -12
  2. package/dist/bi.js +1 -1
  3. package/dist/bi.umd.cjs +76 -89
  4. package/dist/{import-file-DvlE1tB-.js → import-file-BYmPXlRW.js} +9876 -9920
  5. package/dist/{map-component-mixin-CBw16E04.js → map-component-mixin-PRMJIS1C.js} +1546 -1549
  6. package/dist/style.css +1 -1
  7. package/dist/{vs-donut-DMLTafsi.js → vs-donut-CqOel5Tz.js} +7 -7
  8. package/dist/{vs-funnel-bar-DGFJ8biS.js → vs-funnel-bar-DlkEo68l.js} +5 -5
  9. package/dist/{vs-map-CigyNoPm.js → vs-map-B6x6GV6M.js} +3 -3
  10. package/dist/{vs-map-cluster-DS-uWqwB.js → vs-map-cluster-Dz5xmauq.js} +3 -3
  11. package/dist/{vs-number-CkGMYlM9.js → vs-number-DMpjdJl7.js} +3 -3
  12. package/dist/{vs-table-D8yc91eR.js → vs-table-W-7bQhfI.js} +6 -6
  13. package/dist/{vs-text-FI-Cowvo.js → vs-text-BS_wjmtb.js} +32 -39
  14. package/package.json +2 -2
  15. package/server/migrations/bi.sql +7 -1
  16. package/server/plugins/docs.js +48 -48
  17. package/server/plugins/vite.js +69 -69
  18. package/server/routes/dashboard/controllers/dashboard.js +2 -2
  19. package/server/routes/dashboard/controllers/dashboard.list.js +1 -0
  20. package/server/routes/dashboard/controllers/utils/yaml.js +11 -11
  21. package/server/routes/data/controllers/data.js +1 -0
  22. package/server/routes/data/controllers/util/chartSQL.js +2 -1
  23. package/server/routes/data/controllers/util/normalizeData.js +61 -61
  24. package/server/routes/edit/controllers/widget.edit.js +10 -2
  25. package/server/routes/map/controllers/cluster.js +110 -110
  26. package/server/routes/map/controllers/clusterVtile.js +166 -166
  27. package/server/routes/map/controllers/geojson.js +127 -127
  28. package/server/routes/map/controllers/map.js +61 -61
  29. package/server/routes/map/controllers/utils/downloadClusterData.js +44 -44
  30. package/server/routes/map/controllers/vtile.js +183 -183
  31. package/utils.js +12 -12
@@ -1,69 +1,69 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import config from '../../config.js';
4
-
5
- const { disableAuth } = config;
6
- const isProduction = process.env.NODE_ENV === 'production';
7
-
8
- async function plugin(fastify) {
9
- // vite server
10
- if (!isProduction) {
11
- const vite = await import('vite');
12
-
13
- const viteServer = await vite.createServer({
14
- server: {
15
- middlewareMode: true,
16
- },
17
- });
18
- // hot reload
19
- viteServer.watcher.on('all', (d, t) => {
20
- if (!t.includes('module') && !t.includes('templates')) return;
21
- // console.log(d, t);
22
- viteServer.ws.send({ type: 'full-reload' });
23
- });
24
-
25
- // this is middleware for vite's dev servert
26
- fastify.addHook('onRequest', async (req, reply) => {
27
- // const { user } = req.session?.passport || {};
28
- const next = () => new Promise((resolve) => {
29
- viteServer.middlewares(req.raw, reply.raw, () => resolve());
30
- });
31
- await next();
32
- });
33
- fastify.get('*', async () => {});
34
- return;
35
- }
36
-
37
- // From Build
38
- fastify.get('*', async (req, reply) => {
39
- // console.log(disableAuth)
40
- if (!req.user && !disableAuth) return reply.redirect('/login');
41
- const stream = fs.createReadStream('dist/index.html');
42
- return reply
43
- .headers({ 'Cache-Control': 'public, no-cache' })
44
- .type('text/html')
45
- .send(stream);
46
- });
47
- fastify.get('/assets/:file', async (req, reply) => {
48
- const stream = fs.createReadStream(`dist/assets/${req.params.file}`);
49
- const ext = path.extname(req.params.file);
50
- const mime = {
51
- '.js': 'text/javascript',
52
- '.css': 'text/css',
53
- '.woff2': 'application/font-woff',
54
- '.png': 'image/png',
55
- }[ext];
56
- // reply.cacheControl('max-age', '1d');
57
- return mime
58
- ? reply
59
- .headers({
60
- 'Cache-Control': 'public, max-age=3600',
61
- 'Content-Encoding': 'identity',
62
- })
63
- .type(mime)
64
- .send(stream)
65
- : stream;
66
- });
67
- }
68
-
69
- export default plugin;
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import config from '../../config.js';
4
+
5
+ const { disableAuth } = config;
6
+ const isProduction = process.env.NODE_ENV === 'production';
7
+
8
+ async function plugin(fastify) {
9
+ // vite server
10
+ if (!isProduction) {
11
+ const vite = await import('vite');
12
+
13
+ const viteServer = await vite.createServer({
14
+ server: {
15
+ middlewareMode: true,
16
+ },
17
+ });
18
+ // hot reload
19
+ viteServer.watcher.on('all', (d, t) => {
20
+ if (!t.includes('module') && !t.includes('templates')) return;
21
+ // console.log(d, t);
22
+ viteServer.ws.send({ type: 'full-reload' });
23
+ });
24
+
25
+ // this is middleware for vite's dev servert
26
+ fastify.addHook('onRequest', async (req, reply) => {
27
+ // const { user } = req.session?.passport || {};
28
+ const next = () => new Promise((resolve) => {
29
+ viteServer.middlewares(req.raw, reply.raw, () => resolve());
30
+ });
31
+ await next();
32
+ });
33
+ fastify.get('*', async () => {});
34
+ return;
35
+ }
36
+
37
+ // From Build
38
+ fastify.get('*', async (req, reply) => {
39
+ // console.log(disableAuth)
40
+ if (!req.user && !disableAuth) return reply.redirect('/login');
41
+ const stream = fs.createReadStream('dist/index.html');
42
+ return reply
43
+ .headers({ 'Cache-Control': 'public, no-cache' })
44
+ .type('text/html')
45
+ .send(stream);
46
+ });
47
+ fastify.get('/assets/:file', async (req, reply) => {
48
+ const stream = fs.createReadStream(`dist/assets/${req.params.file}`);
49
+ const ext = path.extname(req.params.file);
50
+ const mime = {
51
+ '.js': 'text/javascript',
52
+ '.css': 'text/css',
53
+ '.woff2': 'application/font-woff',
54
+ '.png': 'image/png',
55
+ }[ext];
56
+ // reply.cacheControl('max-age', '1d');
57
+ return mime
58
+ ? reply
59
+ .headers({
60
+ 'Cache-Control': 'public, max-age=3600',
61
+ 'Content-Encoding': 'identity',
62
+ })
63
+ .type(mime)
64
+ .send(stream)
65
+ : stream;
66
+ });
67
+ }
68
+
69
+ export default plugin;
@@ -59,8 +59,8 @@ export default async function dashboard({
59
59
 
60
60
  return {
61
61
  ...data || {},
62
- widgets: data?.widgets?.filter(el => el?.widget_id) || [],
63
- panels: data?.panels?.filter(el => el?.widget) || [],
62
+ widgets: data?.widgets?.filter?.(el => el?.widget_id) || [],
63
+ panels: data?.panels?.filter?.(el => el?.widget) || [],
64
64
  geom: !meta?.geom,
65
65
  error:
66
66
  table && !pg.pk?.[table] ? `table pkey not found: ${table}` : undefined,
@@ -25,6 +25,7 @@ export default async function data({
25
25
 
26
26
  const res = {
27
27
  time: Date.now() - time,
28
+ db: pg.options?.database,
28
29
  rows: list,
29
30
  };
30
31
  return res;
@@ -1,11 +1,11 @@
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
+ 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;
@@ -116,6 +116,7 @@ export default async function dataAPI(req, reply) {
116
116
  order,
117
117
  samples,
118
118
  xType,
119
+ fx: widgetData.fx,
119
120
  });
120
121
 
121
122
  if (query.sql) return sql;
@@ -20,6 +20,7 @@ function chart({
20
20
  order,
21
21
  samples,
22
22
  xType,
23
+ fx, // agg function
23
24
  }) {
24
25
  const xCol = x && xType?.includes('[]') ? `unnest(${x})` : x;
25
26
 
@@ -30,7 +31,7 @@ function chart({
30
31
  (el) =>
31
32
  `${metric} filter (where '${el.name.toString().replace(/'/g, "''")}'=${yType?.includes('[]') ? `any(${groupby.replace(/'/g, "''")}::text[])` : groupby.replace(/'/g, "''")}) as "${el.name.toString().replace(/'/g, "''")}"`
32
33
  )
33
- .join(',') || `${metric} as metric`;
34
+ .join(',') || `${fx || metric} as metric`;
34
35
  const sql = `select ${xCol} ${x && xType?.includes('[]') ? `as ${x}` : ''}, ${metricData}
35
36
  from ${table}
36
37
  where ${where}
@@ -1,61 +1,61 @@
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') skip.push(`column not found: ${query[el]}`);
7
- if (!(el === 'groupby' && query[el] === 'null')) delete query[el];
8
- }
9
- });
10
-
11
- if (
12
- !columnTypes.find(
13
- (col) => col.type === 'numeric' && col.name == query.metric
14
- )
15
- ) {
16
- delete query.metric;
17
- }
18
-
19
- const xName = query.x || (Array.isArray(data.x) ? data.x[0] : data.x);
20
- const xType = columnTypes.find((el) => el.name == xName)?.type;
21
-
22
- const granularity =
23
- xType === 'date' || xType?.includes('timestamp')
24
- ? query.granularity || data.granularity || 'year'
25
- : null;
26
-
27
- const x =
28
- (granularity
29
- ? `date_trunc('${granularity}',${xName})::date::text`
30
- : null) || xName;
31
-
32
- const metrics = Array.isArray(data.metrics || data.metric) ? (data.metrics || data.metric) : [data.metrics || data.metric];
33
- const metric =
34
- (query.metric ? `sum(${query.metric})` : null) ||
35
- (metrics.length
36
- ? (metrics
37
- ?.filter((el) => el && columnTypes.find((col) => col.name == (el?.name || el)))
38
- ?.map((el) => el.fx || `${el.operator || 'sum'}(${el.name || el})`)?.join(',') || 'count(*)')
39
- : 'count(*)');
40
-
41
- const yName = metrics?.[0]?.name || metrics?.[0];
42
- const yType = columnTypes.find((el) => el.name == yName)?.type;
43
-
44
- const { cls, table, filterCustom } = data;
45
- const groupby = (query.groupby || data.groupby) === 'null' ? null : (query.groupby || data.groupby);
46
- // const orderby = query.orderby || data.orderby || 'count(*)';
47
-
48
- const custom = query?.filterCustom
49
- ?.split(',')
50
- ?.map((el) => filterCustom?.find((item) => item?.name === el)?.sql)
51
- ?.filter((el) => el)
52
- ?.join(' and ');
53
- const where = `${data.query || '1=1'} and ${custom || 'true'}`;
54
-
55
- const tableSQL = data.tableSQL?.length
56
- ? `(select * from ${data?.table} t ${data.tableSQL.join(' \n ')} where ${where})q`
57
- : undefined;
58
-
59
- return { x, cls, metric, table, where, tableSQL, groupby, xName, xType, yName, yType, error: skip.length ? skip.join(',') : undefined };
60
- }
61
- export default normalizeData;
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') skip.push(`column not found: ${query[el]}`);
7
+ if (!(el === 'groupby' && query[el] === 'null')) delete query[el];
8
+ }
9
+ });
10
+
11
+ if (
12
+ !columnTypes.find(
13
+ (col) => col.type === 'numeric' && col.name == query.metric
14
+ )
15
+ ) {
16
+ delete query.metric;
17
+ }
18
+
19
+ const xName = query.x || (Array.isArray(data.x) ? data.x[0] : data.x);
20
+ const xType = columnTypes.find((el) => el.name == xName)?.type;
21
+
22
+ const granularity =
23
+ xType === 'date' || xType?.includes('timestamp')
24
+ ? query.granularity || data.granularity || 'year'
25
+ : null;
26
+
27
+ const x =
28
+ (granularity
29
+ ? `date_trunc('${granularity}',${xName})::date::text`
30
+ : null) || xName;
31
+
32
+ const metrics = Array.isArray(data.metrics || data.metric) ? (data.metrics || data.metric) : [data.metrics || data.metric];
33
+ const metric =
34
+ (query.metric ? `sum(${query.metric})` : null) ||
35
+ (metrics.length
36
+ ? (metrics
37
+ ?.filter((el) => el && columnTypes.find((col) => col.name == (el?.name || el)))
38
+ ?.map((el) => el.fx || `${el.operator || 'sum'}(${el.name || el})`)?.join(',') || 'count(*)')
39
+ : 'count(*)');
40
+
41
+ const yName = metrics?.[0]?.name || metrics?.[0];
42
+ const yType = columnTypes.find((el) => el.name == yName)?.type;
43
+
44
+ const { cls, table, filterCustom } = data;
45
+ const groupby = (query.groupby || data.groupby) === 'null' ? null : (query.groupby || data.groupby);
46
+ // const orderby = query.orderby || data.orderby || 'count(*)';
47
+
48
+ const custom = query?.filterCustom
49
+ ?.split(',')
50
+ ?.map((el) => filterCustom?.find((item) => item?.name === el)?.sql)
51
+ ?.filter((el) => el)
52
+ ?.join(' and ');
53
+ const where = `${data.query || '1=1'} and ${custom || 'true'}`;
54
+
55
+ const tableSQL = data.tableSQL?.length
56
+ ? `(select * from ${data?.table} t ${data.tableSQL.join(' \n ')} where ${where})q`
57
+ : undefined;
58
+
59
+ return { x, cls, metric, table, where, tableSQL, groupby, xName, xType, yName, yType, error: skip.length ? skip.join(',') : undefined };
60
+ }
61
+ export default normalizeData;
@@ -32,11 +32,10 @@ export default async function widgetEdit({ pg = pgClients.client, body, params }
32
32
  ?.reduce?.((acc, curr) => ({ ...acc, [curr]: data.data[curr] === 'null' ? null : data.data[curr] }), {})
33
33
  });
34
34
  }
35
- const widgetData = ['style', 'data', 'type', 'title', 'controls']
35
+ const widgetData = ['style', 'data', 'type', 'title', 'controls', 'query', 'cls', 'x']
36
36
  .filter(el => data[el])
37
37
  .reduce((p, el) => ({ ...p, [el]: data[el] }), {});
38
38
 
39
- //console.log(templateData)
40
39
  // get table
41
40
  const tableName = body.table || widgetData.data?.table_name || widgetData.data?.table || widgetData?.table_name || templateData.table;
42
41
  if (!tableName || !(pg.pk?.[tableName] || pgClients[widgetData?.data?.db || templateData?.db || 'client']?.pk?.[tableName])) {
@@ -76,6 +75,15 @@ export default async function widgetEdit({ pg = pgClients.client, body, params }
76
75
  if (widgetData.data?.type === 'text' && idx > -1) {
77
76
  widgets[idx].data.text = widgetData.data?.text;
78
77
  }
78
+ if (typeof widgetData?.query === 'string' && idx > -1) {
79
+ widgets[idx].data.query = widgetData.query;
80
+ }
81
+ if (typeof widgetData?.cls === 'string' && idx > -1) {
82
+ widgets[idx].data.cls = widgetData.cls;
83
+ }
84
+ if (typeof widgetData?.x === 'string' && idx > -1) {
85
+ widgets[idx].data.x = widgetData.x;
86
+ }
79
87
 
80
88
  await dataUpdate({
81
89
  pg,
@@ -1,110 +1,110 @@
1
- import { getFilterSQL, logger, pgClients } from '@opengis/fastify-table/utils.js';
2
-
3
- import { getWidget } from '../../../../utils.js';
4
-
5
- import downloadClusterData from './utils/downloadClusterData.js';
6
-
7
- const clusterExists = {};
8
-
9
- export default async function cluster(req) {
10
- const { query = {} } = req;
11
- const { widget, filter, dashboard, search } = query;
12
-
13
- if (!widget) {
14
- return { message: 'not enough params: widget', status: 400 };
15
- }
16
-
17
- const { pg = req.pg || pgClients.client, data } = await getWidget({ pg: req.pg, dashboard, widget });
18
-
19
- const pkey = pg.pk?.[data?.table];
20
-
21
- if (!pkey) {
22
- return {
23
- message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`,
24
- status: 400,
25
- };
26
- }
27
-
28
- // data param
29
- const {
30
- table,
31
- query: where = '1=1',
32
- metrics = [],
33
- cluster,
34
- clusterTable = {},
35
- } = data;
36
-
37
- if (!cluster) {
38
- return {
39
- message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`,
40
- status: 400,
41
- };
42
- }
43
-
44
- if (!metrics.length) {
45
- return {
46
- message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`,
47
- status: 400,
48
- };
49
- }
50
-
51
- if (!clusterTable?.name) {
52
- Object.assign(clusterTable, {
53
- name: 'bi.cluster',
54
- title: 'title',
55
- query: `type='${cluster}'`,
56
- });
57
- }
58
-
59
- try {
60
- if (cluster && !clusterExists[cluster]) {
61
- const res = await downloadClusterData({ pg, cluster });
62
- if (res) return res;
63
- clusterExists[cluster] = 1;
64
- }
65
-
66
- if (clusterTable?.name && !pg.pk?.[clusterTable?.name]) {
67
- return {
68
- message: 'invalid widget params: clusterTable pkey not found',
69
- status: 404,
70
- };
71
- }
72
-
73
- const { bounds, extentStr } = await pg.query(`select count(*),
74
- st_asgeojson(st_extent(geom))::json as bounds,
75
- replace(regexp_replace(st_extent(geom)::box2d::text,'BOX\\(|\\)','','g'),' ',',') as "extentStr"
76
- from ${table} where ${where || '1=1'}`).then((res) => res.rows?.[0] || {});
77
- const extent = extentStr ? extentStr.split(',') : undefined;
78
-
79
- // get sql
80
- const { optimizedSQL } =
81
- filter || search
82
- ? await getFilterSQL({ pg, table, filter, search })
83
- : {};
84
-
85
- const q = `select b.*, ${clusterTable?.operator || 'sum'}("${metrics[0]}")::float as metric
86
- from ${optimizedSQL ? `(${optimizedSQL})` : table} q
87
- left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id, ${clusterTable?.column || cluster} as name, ${clusterTable?.title} as title from ${clusterTable?.name} where ${clusterTable?.codifierColumn || 'codifier'}=q."${clusterTable?.column || cluster}" limit 1)b on 1=1
88
- where ${where} group by b.id, b.name, b.title order by ${clusterTable?.operator || 'sum'}("${metrics[0]}")::float desc`;
89
-
90
- if (query.sql === '1') return q;
91
-
92
- // auto Index
93
- // autoIndex({ table, columns: (metrics || []).concat([cluster]) });
94
-
95
- const { rows = [] } = await pg.query(q);
96
- const vals = rows.map((el) => el.metric - 0).sort((a, b) => a - b);
97
- const len = vals.length;
98
- const sizes = [
99
- vals[0],
100
- vals[Math.floor(len / 4)],
101
- vals[Math.floor(len / 2)],
102
- vals[Math.floor(len * 0.75)],
103
- vals[len - 1],
104
- ];
105
- return { sizes, rows, bounds, extent, count: rows.length, total: rows?.reduce((acc, curr) => (curr.metric || 0) + acc, 0) };
106
- } catch (err) {
107
- logger.file('bi/cluster/error', { error: err.toString(), query });
108
- return { error: err.toString(), status: 500 };
109
- }
110
- }
1
+ import { getFilterSQL, logger, pgClients } from '@opengis/fastify-table/utils.js';
2
+
3
+ import { getWidget } from '../../../../utils.js';
4
+
5
+ import downloadClusterData from './utils/downloadClusterData.js';
6
+
7
+ const clusterExists = {};
8
+
9
+ export default async function cluster(req) {
10
+ const { query = {} } = req;
11
+ const { widget, filter, dashboard, search } = query;
12
+
13
+ if (!widget) {
14
+ return { message: 'not enough params: widget', status: 400 };
15
+ }
16
+
17
+ const { pg = req.pg || pgClients.client, data } = await getWidget({ pg: req.pg, dashboard, widget });
18
+
19
+ const pkey = pg.pk?.[data?.table];
20
+
21
+ if (!pkey) {
22
+ return {
23
+ message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`,
24
+ status: 400,
25
+ };
26
+ }
27
+
28
+ // data param
29
+ const {
30
+ table,
31
+ query: where = '1=1',
32
+ metrics = [],
33
+ cluster,
34
+ clusterTable = {},
35
+ } = data;
36
+
37
+ if (!cluster) {
38
+ return {
39
+ message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`,
40
+ status: 400,
41
+ };
42
+ }
43
+
44
+ if (!metrics.length) {
45
+ return {
46
+ message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`,
47
+ status: 400,
48
+ };
49
+ }
50
+
51
+ if (!clusterTable?.name) {
52
+ Object.assign(clusterTable, {
53
+ name: 'bi.cluster',
54
+ title: 'title',
55
+ query: `type='${cluster}'`,
56
+ });
57
+ }
58
+
59
+ try {
60
+ if (cluster && !clusterExists[cluster]) {
61
+ const res = await downloadClusterData({ pg, cluster });
62
+ if (res) return res;
63
+ clusterExists[cluster] = 1;
64
+ }
65
+
66
+ if (clusterTable?.name && !pg.pk?.[clusterTable?.name]) {
67
+ return {
68
+ message: 'invalid widget params: clusterTable pkey not found',
69
+ status: 404,
70
+ };
71
+ }
72
+
73
+ const { bounds, extentStr } = await pg.query(`select count(*),
74
+ st_asgeojson(st_extent(geom))::json as bounds,
75
+ replace(regexp_replace(st_extent(geom)::box2d::text,'BOX\\(|\\)','','g'),' ',',') as "extentStr"
76
+ from ${table} where ${where || '1=1'}`).then((res) => res.rows?.[0] || {});
77
+ const extent = extentStr ? extentStr.split(',') : undefined;
78
+
79
+ // get sql
80
+ const { optimizedSQL } =
81
+ filter || search
82
+ ? await getFilterSQL({ pg, table, filter, search })
83
+ : {};
84
+
85
+ const q = `select b.*, ${clusterTable?.operator || 'sum'}("${metrics[0]}")::float as metric
86
+ from ${optimizedSQL ? `(${optimizedSQL})` : table} q
87
+ left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id, ${clusterTable?.column || cluster} as name, ${clusterTable?.title} as title from ${clusterTable?.name} where ${clusterTable?.codifierColumn || 'codifier'}=q."${clusterTable?.column || cluster}" limit 1)b on 1=1
88
+ where ${where} group by b.id, b.name, b.title order by ${clusterTable?.operator || 'sum'}("${metrics[0]}")::float desc`;
89
+
90
+ if (query.sql === '1') return q;
91
+
92
+ // auto Index
93
+ // autoIndex({ table, columns: (metrics || []).concat([cluster]) });
94
+
95
+ const { rows = [] } = await pg.query(q);
96
+ const vals = rows.map((el) => el.metric - 0).sort((a, b) => a - b);
97
+ const len = vals.length;
98
+ const sizes = [
99
+ vals[0],
100
+ vals[Math.floor(len / 4)],
101
+ vals[Math.floor(len / 2)],
102
+ vals[Math.floor(len * 0.75)],
103
+ vals[len - 1],
104
+ ];
105
+ return { sizes, rows, bounds, extent, count: rows.length, total: rows?.reduce((acc, curr) => (curr.metric || 0) + acc, 0) };
106
+ } catch (err) {
107
+ logger.file('bi/cluster/error', { error: err.toString(), query });
108
+ return { error: err.toString(), status: 500 };
109
+ }
110
+ }