@opengis/bi 1.0.14 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +50 -50
  2. package/config.js +12 -12
  3. package/dist/bi.js +1 -1
  4. package/dist/bi.umd.cjs +63 -63
  5. package/dist/{import-file-DUp3rsNI.js → import-file-CRC0sYYT.js} +8055 -7987
  6. package/dist/{map-component-mixin-CGM0P5ub.js → map-component-mixin-BCtWEvzv.js} +3795 -2116
  7. package/dist/style.css +1 -1
  8. package/dist/{vs-calendar-cOoinEwc.js → vs-calendar-5ot79n0N.js} +20 -9
  9. package/dist/{vs-funnel-bar-kLkPoIhJ.js → vs-funnel-bar-CLo6gXI_.js} +2 -2
  10. package/dist/{vs-heatmap-3XAVGTSo.js → vs-heatmap-DHGA8dRk.js} +3 -4
  11. package/dist/{vs-map-cluster-BWJPx7wE.js → vs-map-cluster-CNgX6JVF.js} +2 -2
  12. package/dist/{vs-map-B1tr6V5_.js → vs-map-pIn5wS4G.js} +2 -2
  13. package/dist/{vs-number-CrU7LmkV.js → vs-number-DYfok8VU.js} +19 -12
  14. package/dist/{vs-text-DRPx3aID.js → vs-text-Dckykz09.js} +19 -14
  15. package/package.json +107 -97
  16. package/plugin.js +14 -13
  17. package/server/migrations/bi.dataset.sql +26 -0
  18. package/server/migrations/bi.sql +93 -93
  19. package/server/plugins/docs.js +48 -48
  20. package/server/plugins/hook.js +89 -89
  21. package/server/plugins/vite.js +69 -69
  22. package/server/routes/dashboard/controllers/dashboard.delete.js +38 -37
  23. package/server/routes/dashboard/controllers/dashboard.js +118 -114
  24. package/server/routes/dashboard/controllers/dashboard.list.js +30 -36
  25. package/server/routes/dashboard/controllers/utils/yaml.js +11 -11
  26. package/server/routes/dashboard/index.mjs +25 -25
  27. package/server/routes/data/controllers/data.js +167 -156
  28. package/server/routes/data/controllers/util/chartSQL.js +42 -39
  29. package/server/routes/data/controllers/util/normalizeData.js +59 -56
  30. package/server/routes/data/index.mjs +29 -24
  31. package/server/routes/dataset/controllers/bi.dataset.demo.add.js +97 -0
  32. package/server/routes/dataset/controllers/bi.dataset.import.js +67 -0
  33. package/server/routes/dataset/controllers/util/create.table.js +22 -0
  34. package/server/routes/dataset/controllers/util/prepare.data.js +49 -0
  35. package/server/routes/dataset/index.mjs +19 -0
  36. package/server/routes/db/controllers/dbTablePreview.js +63 -63
  37. package/server/routes/db/controllers/dbTables.js +36 -36
  38. package/server/routes/db/index.mjs +17 -17
  39. package/server/routes/edit/controllers/dashboard.add.js +26 -24
  40. package/server/routes/edit/controllers/dashboard.edit.js +46 -44
  41. package/server/routes/edit/controllers/widget.add.js +75 -73
  42. package/server/routes/edit/controllers/widget.del.js +69 -70
  43. package/server/routes/edit/controllers/widget.edit.js +52 -103
  44. package/server/routes/edit/index.mjs +31 -31
  45. package/server/routes/map/controllers/cluster.js +109 -104
  46. package/server/routes/map/controllers/clusterVtile.js +166 -213
  47. package/server/routes/map/controllers/geojson.js +127 -127
  48. package/server/routes/map/controllers/map.js +60 -57
  49. package/server/routes/map/controllers/utils/downloadClusterData.js +43 -0
  50. package/server/routes/map/controllers/vtile.js +183 -182
  51. package/server/routes/map/index.mjs +25 -25
  52. package/server/utils/getWidget.js +85 -83
  53. package/utils.js +12 -12
@@ -1,104 +1,109 @@
1
- import { getWidget } from '../../../../utils.js';
2
-
3
- export default async function cluster({ pg, funcs, query = {}, log }) {
4
- const { widget, filter, dashboard, search } = query;
5
-
6
- if (!widget) {
7
- return { message: 'not enough params: widget', status: 400 };
8
- }
9
-
10
- const { data } = await getWidget({ dashboard, widget });
11
-
12
- try {
13
- const pkey = pg.pk?.[data?.table];
14
-
15
- if (!pkey) {
16
- return {
17
- message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`,
18
- status: 400,
19
- };
20
- }
21
-
22
- // data param
23
- const {
24
- table,
25
- query: where = '1=1',
26
- metrics = [],
27
- cluster,
28
- clusterTable = {},
29
- } = data;
30
-
31
- if (!cluster) {
32
- return {
33
- message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`,
34
- status: 400,
35
- };
36
- }
37
-
38
- if (!metrics.length) {
39
- return {
40
- message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`,
41
- status: 400,
42
- };
43
- }
44
-
45
- if (!clusterTable?.name) {
46
- return {
47
- message: 'not enough widget params: clusterTable name',
48
- status: 400,
49
- };
50
- }
51
-
52
- if (clusterTable?.name && !pg.pk?.[clusterTable?.name]) {
53
- return {
54
- message: 'invalid widget params: clusterTable pkey not found',
55
- status: 404,
56
- };
57
- }
58
-
59
- const { total = 0 } = pg.pk?.[table]
60
- ? await pg
61
- .queryCache(
62
- `select oid::regclass as table, reltuples AS total from pg_class`
63
- )
64
- .then((res) => res.rows?.find((row) => row.table === table))
65
- : {};
66
-
67
- const { bbox } = await pg
68
- .query(
69
- `select st_asgeojson(box2d(geom))::json as bbox from ${table} where ${where || '1=1'}`
70
- )
71
- .then((res1) => res1.rows?.[0] || {});
72
-
73
- // get sql
74
- const { optimizedSQL } =
75
- filter || search
76
- ? await funcs.getFilterSQL({ pg, table, filter, search })
77
- : {};
78
-
79
- const q = `select "${cluster}" as name, sum("${metrics[0]}")::float as metric
80
- from ${optimizedSQL ? `(${optimizedSQL})` : table} q
81
- left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id from ${clusterTable?.name} where ${clusterTable?.title}=q."${cluster}" limit 1)b on 1=1
82
- where ${where} group by ${cluster}, b.id`;
83
-
84
- if (query.sql === '1') return q;
85
-
86
- // auto Index
87
- // funcs.autoIndex({ table, columns: (metrics || []).concat([cluster]) });
88
-
89
- const { rows = [] } = await pg.query(q);
90
- const vals = rows.map((el) => el.metric - 0).sort((a, b) => a - b);
91
- const len = vals.length;
92
- const sizes = [
93
- vals[0],
94
- vals[Math.floor(len / 4)],
95
- vals[Math.floor(len / 2)],
96
- vals[Math.floor(len * 0.75)],
97
- vals[len - 1],
98
- ];
99
- return { sizes, rows, bbox, count: rows.length, total };
100
- } catch (err) {
101
- log.error('bi/cluster', { error: err.toString(), query });
102
- return { error: err.toString(), status: 500 };
103
- }
104
- }
1
+ import { getFilterSQL } 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({ pg, query = {}, log }) {
10
+ const { widget, filter, dashboard, search } = query;
11
+
12
+ if (!widget) {
13
+ return { message: 'not enough params: widget', status: 400 };
14
+ }
15
+
16
+ const { data } = await getWidget({ dashboard, widget });
17
+
18
+ try {
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
+ if (cluster && !clusterExists[cluster]) {
60
+ const res = await downloadClusterData({ pg, log, cluster });
61
+ if (res) return res;
62
+ clusterExists[cluster] = 1;
63
+ }
64
+
65
+ if (clusterTable?.name && !pg.pk?.[clusterTable?.name]) {
66
+ return {
67
+ message: 'invalid widget params: clusterTable pkey not found',
68
+ status: 404,
69
+ };
70
+ }
71
+
72
+ const { bounds, extentStr } = await pg.query(`select count(*),
73
+ st_asgeojson(st_extent(geom))::json as bounds,
74
+ replace(regexp_replace(st_extent(geom)::box2d::text,'BOX\\(|\\)','','g'),' ',',') as "extentStr"
75
+ from ${table} where ${where || '1=1'}`).then((res) => res.rows?.[0] || {});
76
+ const extent = extentStr ? extentStr.split(',') : undefined;
77
+
78
+ // get sql
79
+ const { optimizedSQL } =
80
+ filter || search
81
+ ? await getFilterSQL({ pg, table, filter, search })
82
+ : {};
83
+
84
+ const q = `select "${cluster}" as name, sum("${metrics[0]}")::float as metric
85
+ from ${optimizedSQL ? `(${optimizedSQL})` : table} q
86
+ left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id from ${clusterTable?.name} where ${clusterTable?.title}=q."${cluster}" limit 1)b on 1=1
87
+ where ${where} group by ${cluster}, b.id order by sum("${metrics[0]}")::float desc`;
88
+
89
+ if (query.sql === '1') return q;
90
+
91
+ // auto Index
92
+ // autoIndex({ table, columns: (metrics || []).concat([cluster]) });
93
+
94
+ const { rows = [] } = await pg.query(q);
95
+ const vals = rows.map((el) => el.metric - 0).sort((a, b) => a - b);
96
+ const len = vals.length;
97
+ const sizes = [
98
+ vals[0],
99
+ vals[Math.floor(len / 4)],
100
+ vals[Math.floor(len / 2)],
101
+ vals[Math.floor(len * 0.75)],
102
+ vals[len - 1],
103
+ ];
104
+ return { sizes, rows, bounds, extent, count: rows.length, total: rows?.reduce((acc, curr) => (curr.metric || 0) + acc, 0) };
105
+ } catch (err) {
106
+ log.error('bi/cluster', { error: err.toString(), query });
107
+ return { error: err.toString(), status: 500 };
108
+ }
109
+ }
@@ -1,213 +1,166 @@
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 { pg, funcs, params = {}, query = {}, log } = req;
15
- const { z, y } = params;
16
- const x = params.x?.split('.')[0] - 0;
17
-
18
- if (!x || !y || !z) {
19
- return { message: 'not enough params: xyz', status: 400 };
20
- }
21
-
22
- const { widget, filter, dashboard, search, clusterZoom, nocache, pointZoom } =
23
- query;
24
-
25
- if (!widget) {
26
- return { message: 'not enough params: widget', status: 400 };
27
- }
28
-
29
- const { data } = await getWidget({ dashboard, widget });
30
-
31
- const headers = {
32
- 'Content-Type': 'application/x-protobuf',
33
- 'Cache-Control':
34
- nocache || query.sql ? 'no-cache' : 'public, max-age=86400',
35
- };
36
-
37
- const hash = [pointZoom, filter].filter((el) => el).join();
38
-
39
- const root = funcs.getFolder(req);
40
- const file = path.join(
41
- root,
42
- `/map/vtile/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}${z}/${x}/${y}.mvt`
43
- );
44
-
45
- try {
46
- if (!data?.table) {
47
- return {
48
- message: `invalid ${widget ? 'widget' : 'dashboard'}: table not specified`,
49
- status: 400,
50
- };
51
- }
52
-
53
- const pkey = pg.pk?.[data?.table];
54
-
55
- if (!pkey) {
56
- return {
57
- message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`,
58
- status: 400,
59
- };
60
- }
61
-
62
- // data param
63
- const {
64
- table,
65
- query: where = '1=1',
66
- metrics = [],
67
- cluster,
68
- clusterTable = {},
69
- } = data;
70
- if (!clusterTable?.name) {
71
- Object.assign(clusterTable, {
72
- name: 'bi.cluster',
73
- title: 'title',
74
- query: `type='${data.cluster}'`,
75
- });
76
- }
77
-
78
- if (cluster && !clusterExists[data.cluster]) {
79
- const res = await fetch(
80
- `https://cdn.softpro.ua/data/bi/${data.cluster}-ua.geojson`
81
- );
82
- if (res?.status !== 200) {
83
- return {
84
- message: `cluster file not found: ${cluster}-ua.json`,
85
- status: 404,
86
- };
87
- }
88
- try {
89
- const geojson = await res.json();
90
-
91
- const features = geojson?.features?.filter(
92
- (el, idx, arr) =>
93
- el?.geometry &&
94
- arr
95
- .map((item) => item.properties.name)
96
- .indexOf(el.properties.name) === idx
97
- ); // unique
98
- if (!features?.length) {
99
- return {
100
- message: `cluster file empty: ${cluster}-ua.json`,
101
- status: 400,
102
- };
103
- }
104
- const { count = 0 } = await pg
105
- .query(`select count(*)::int from bi.cluster where type=$1`, [
106
- cluster,
107
- ])
108
- .then((res1) => res1.rows?.[0] || {});
109
- if (count !== features?.length) {
110
- // await pg.query(`delete from bi.cluster where type=$1`, [cluster]);
111
- const values = features
112
- ?.map(
113
- (el) =>
114
- `('${el.properties.name?.replace(/'/g, "''") || ''}', '${cluster}', ST_GeomFromGeoJSON('${JSON.stringify(el.geometry)}')::geometry)`
115
- )
116
- .join(',');
117
- const { rowCount } = await pg.query(
118
- `insert into bi.cluster (title,type,geom) values ${values} on conflict(title,type) do update set geom=excluded.geom`
119
- );
120
- log.info('bi/clusterVtile', { cluster, rowCount });
121
- }
122
- clusterExists[cluster] = 1;
123
- } catch (err) {
124
- log.error('bi/clusterVtile', {
125
- error: err.toString(),
126
- filename: `${cluster}-ua.json`,
127
- });
128
- return {
129
- message: `cluster file import error: ${cluster}-ua.json`,
130
- status: 500,
131
- };
132
- }
133
- }
134
-
135
- if (!cluster) {
136
- return {
137
- message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`,
138
- status: 400,
139
- };
140
- }
141
-
142
- if (!metrics.length) {
143
- return {
144
- message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`,
145
- status: 400,
146
- };
147
- }
148
-
149
- // get sql
150
- const { optimizedSQL } =
151
- filter || search
152
- ? await funcs.getFilterSQL({ pg, table, filter, search })
153
- : {};
154
-
155
- const q = `select "${cluster}" as name, sum("${metrics[0]}")::float as metric, b.*
156
- from ${optimizedSQL ? `(${optimizedSQL})` : table} q
157
- left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id,
158
- ${clusterTable?.geom || 'geom'} as geom from ${clusterTable?.name}
159
- where ${clusterTable?.query || '1=1'} and ${clusterTable?.title}=q."${cluster}" limit 1
160
- )b on 1=1
161
- where ${where} group by
162
- ${cluster}, b.id, b.${clusterTable?.geom || 'geom'}`;
163
-
164
- if (query.sql === '1') return q;
165
-
166
- const geomCol =
167
- parseInt(z, 10) < parseInt(pointZoom, 10)
168
- ? `ST_Centroid(${clusterTable?.geom || data?.geom || 'geom'})`
169
- : clusterTable?.geom || data?.geom || 'geom';
170
-
171
- const bbox = mercator.bbox(+y, +x, +z, false /* , '900913' */);
172
- const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;
173
-
174
- const q1 = `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
175
- FROM (
176
- SELECT
177
- floor(random() * 100000 + 1)::int + row_number() over() as row,
178
-
179
- ${pg.pk?.[clusterTable?.name] ? 'id,' : ''} name, metric,
180
-
181
- ST_AsMVTGeom(st_transform(${geomCol}, 3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
182
-
183
- FROM (select * from (${q})q where geom && ${bbox2d}
184
-
185
- and geom is not null and st_srid(geom) >0
186
-
187
- and ST_GeometryType(geom) = any ('{ "ST_Polygon", "ST_MultiPolygon" }')
188
-
189
- limit 3000)q
190
- ) q`;
191
-
192
- if (query.sql === '2') return q1;
193
-
194
- // auto Index
195
- funcs.autoIndex({ table, columns: (metrics || []).concat([cluster]) });
196
-
197
- const { rows = [] } = await pg.query(q1);
198
-
199
- if (query.sql === '3') return rows.map((el) => el.tile);
200
-
201
- const buffer = Buffer.concat(rows.map((el) => Buffer.from(el.tile)));
202
-
203
- if (!nocache) {
204
- await mkdir(path.dirname(file), { recursive: true });
205
- await writeFile(file, buffer, 'binary');
206
- }
207
-
208
- return reply.headers(headers).send(buffer);
209
- } catch (err) {
210
- log.error('bi/clusterVtile', { error: err.toString(), query, params });
211
- return { error: err.toString(), status: 500 };
212
- }
213
- }
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 { getFolder, getFilterSQL, autoIndex } from '@opengis/fastify-table/utils.js';
8
+
9
+ import { getWidget } from '../../../../utils.js';
10
+
11
+ import downloadClusterData from './utils/downloadClusterData.js';
12
+
13
+ const mercator = new Sphericalmercator({ size: 256 });
14
+
15
+ const clusterExists = {};
16
+
17
+ export default async function clusterVtile(req, reply) {
18
+ const { pg, funcs, params = {}, query = {}, log } = req;
19
+ const { z, y } = params;
20
+ const x = params.x?.split('.')[0] - 0;
21
+
22
+ if (!x || !y || !z) {
23
+ return { message: 'not enough params: xyz', status: 400 };
24
+ }
25
+
26
+ const { widget, filter, dashboard, search, clusterZoom, nocache, pointZoom } =
27
+ query;
28
+
29
+ if (!widget) {
30
+ return { message: 'not enough params: widget', status: 400 };
31
+ }
32
+
33
+ const { data } = await getWidget({ dashboard, widget });
34
+
35
+ const headers = {
36
+ 'Content-Type': 'application/x-protobuf',
37
+ 'Cache-Control':
38
+ nocache || query.sql ? 'no-cache' : 'public, max-age=86400',
39
+ };
40
+
41
+ const hash = [pointZoom, filter].filter((el) => el).join();
42
+
43
+ const root = getFolder(req);
44
+ const file = path.join(
45
+ root,
46
+ `/map/vtile/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}${z}/${x}/${y}.mvt`
47
+ );
48
+
49
+ try {
50
+ if (!data?.table) {
51
+ return {
52
+ message: `invalid ${widget ? 'widget' : 'dashboard'}: table not specified`,
53
+ status: 400,
54
+ };
55
+ }
56
+
57
+ const pkey = pg.pk?.[data?.table];
58
+
59
+ if (!pkey) {
60
+ return {
61
+ message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`,
62
+ status: 400,
63
+ };
64
+ }
65
+
66
+ // data param
67
+ const {
68
+ table,
69
+ query: where = '1=1',
70
+ metrics = [],
71
+ cluster,
72
+ clusterTable = {},
73
+ } = data;
74
+ if (!clusterTable?.name) {
75
+ Object.assign(clusterTable, {
76
+ name: 'bi.cluster',
77
+ title: 'title',
78
+ query: `type='${data.cluster}'`,
79
+ });
80
+ }
81
+
82
+ if (cluster && !clusterExists[data.cluster]) {
83
+ const res = await downloadClusterData({ pg, log, cluster });
84
+ if (res) return res;
85
+ clusterExists[cluster] = 1;
86
+ }
87
+
88
+ if (!cluster) {
89
+ return {
90
+ message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`,
91
+ status: 400,
92
+ };
93
+ }
94
+
95
+ if (!metrics.length) {
96
+ return {
97
+ message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`,
98
+ status: 400,
99
+ };
100
+ }
101
+
102
+ // get sql
103
+ const { optimizedSQL } =
104
+ filter || search
105
+ ? await getFilterSQL({ pg, table, filter, search })
106
+ : {};
107
+
108
+ const q = `select "${cluster}" as name, sum("${metrics[0]}")::float as metric, b.*
109
+ from ${optimizedSQL ? `(${optimizedSQL})` : table} q
110
+ left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id,
111
+ ${clusterTable?.geom || 'geom'} as geom from ${clusterTable?.name}
112
+ where ${clusterTable?.query || '1=1'} and ${clusterTable?.title}=q."${cluster}" limit 1
113
+ )b on 1=1
114
+ where ${where} group by
115
+ ${cluster}, b.id, b.${clusterTable?.geom || 'geom'}`;
116
+
117
+ if (query.sql === '1') return q;
118
+
119
+ const geomCol =
120
+ parseInt(z, 10) < parseInt(pointZoom, 10)
121
+ ? `ST_Centroid(${clusterTable?.geom || data?.geom || 'geom'})`
122
+ : clusterTable?.geom || data?.geom || 'geom';
123
+
124
+ const bbox = mercator.bbox(+y, +x, +z, false /* , '900913' */);
125
+ const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;
126
+
127
+ const q1 = `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
128
+ FROM (
129
+ SELECT
130
+ floor(random() * 100000 + 1)::int + row_number() over() as row,
131
+
132
+ ${pg.pk?.[clusterTable?.name] ? 'id,' : ''} name, metric,
133
+
134
+ ST_AsMVTGeom(st_transform(${geomCol}, 3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
135
+
136
+ FROM (select * from (${q})q where geom && ${bbox2d}
137
+
138
+ and geom is not null and st_srid(geom) >0
139
+
140
+ and ST_GeometryType(geom) = any ('{ "ST_Polygon", "ST_MultiPolygon" }')
141
+
142
+ limit 3000)q
143
+ ) q`;
144
+
145
+ if (query.sql === '2') return q1;
146
+
147
+ // auto Index
148
+ autoIndex({ table, columns: (metrics || []).concat([cluster]) });
149
+
150
+ const { rows = [] } = await pg.query(q1);
151
+
152
+ if (query.sql === '3') return rows.map((el) => el.tile);
153
+
154
+ const buffer = Buffer.concat(rows.map((el) => Buffer.from(el.tile)));
155
+
156
+ if (!nocache) {
157
+ await mkdir(path.dirname(file), { recursive: true });
158
+ await writeFile(file, buffer, 'binary');
159
+ }
160
+
161
+ return reply.headers(headers).send(buffer);
162
+ } catch (err) {
163
+ log.error('bi/clusterVtile', { error: err.toString(), query, params });
164
+ return { error: err.toString(), status: 500 };
165
+ }
166
+ }