@opengis/bi 1.0.13 → 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 (58) hide show
  1. package/README.md +50 -52
  2. package/config.js +12 -12
  3. package/dist/bi.js +1 -1
  4. package/dist/bi.umd.cjs +120 -134
  5. package/dist/{import-file-1T7kpSzt.js → import-file-CRC0sYYT.js} +11974 -11522
  6. package/dist/{map-component-mixin-BLM9iEWA.js → map-component-mixin-BCtWEvzv.js} +4830 -3150
  7. package/dist/style.css +1 -1
  8. package/dist/vs-calendar-5ot79n0N.js +110 -0
  9. package/dist/vs-funnel-bar-CLo6gXI_.js +105 -0
  10. package/dist/vs-heatmap-DHGA8dRk.js +97 -0
  11. package/dist/{vs-map-cluster-Dfe9INqE.js → vs-map-cluster-CNgX6JVF.js} +28 -25
  12. package/dist/vs-map-pIn5wS4G.js +74 -0
  13. package/dist/vs-number-DYfok8VU.js +55 -0
  14. package/dist/{vs-text-DcrAdQ40.js → vs-text-Dckykz09.js} +19 -13
  15. package/package.json +107 -72
  16. package/plugin.js +14 -13
  17. package/server/migrations/bi.dataset.sql +26 -0
  18. package/server/migrations/bi.sql +93 -27
  19. package/server/plugins/docs.js +48 -47
  20. package/server/plugins/hook.js +89 -86
  21. package/server/plugins/vite.js +69 -55
  22. package/server/routes/dashboard/controllers/dashboard.delete.js +38 -35
  23. package/server/routes/dashboard/controllers/dashboard.js +118 -80
  24. package/server/routes/dashboard/controllers/dashboard.list.js +30 -39
  25. package/server/routes/dashboard/controllers/utils/yaml.js +11 -12
  26. package/server/routes/dashboard/index.mjs +25 -24
  27. package/server/routes/data/controllers/data.js +168 -97
  28. package/server/routes/data/controllers/util/chartSQL.js +42 -25
  29. package/server/routes/data/controllers/util/normalizeData.js +59 -34
  30. package/server/routes/data/index.mjs +29 -26
  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 -0
  37. package/server/routes/db/controllers/dbTables.js +36 -0
  38. package/server/routes/db/index.mjs +17 -0
  39. package/server/routes/edit/controllers/dashboard.add.js +26 -23
  40. package/server/routes/edit/controllers/dashboard.edit.js +46 -37
  41. package/server/routes/edit/controllers/widget.add.js +75 -49
  42. package/server/routes/edit/controllers/widget.del.js +69 -63
  43. package/server/routes/edit/controllers/widget.edit.js +52 -82
  44. package/server/routes/edit/index.mjs +31 -27
  45. package/server/routes/map/controllers/cluster.js +109 -75
  46. package/server/routes/map/controllers/clusterVtile.js +166 -143
  47. package/server/routes/map/controllers/geojson.js +127 -101
  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 -161
  51. package/server/routes/map/index.mjs +25 -25
  52. package/server/utils/getWidget.js +85 -56
  53. package/utils.js +12 -11
  54. package/dist/vs-calendar-WiK1hcHS.js +0 -96
  55. package/dist/vs-funnel-bar-CpPbYZ0_.js +0 -92
  56. package/dist/vs-heatmap-BG4eIROH.js +0 -83
  57. package/dist/vs-map-BRk6Fmks.js +0 -66
  58. package/dist/vs-number-CJq-vi95.js +0 -39
@@ -1,143 +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 {
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
- }
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
+ }
@@ -1,101 +1,127 @@
1
- import Sphericalmercator from '@mapbox/sphericalmercator';
2
-
3
- import path from 'path';
4
- import { createHash } from 'crypto';
5
- import { writeFile, mkdir, readFile, stat } from 'fs/promises';
6
- import { existsSync, /*readdirSync, */ readFileSync } from 'fs';
7
-
8
- import yaml from '../../dashboard/controllers/utils/yaml.js';
9
- import normalizeData from '../../data/controllers/util/normalizeData.js';
10
-
11
- import { getWidget } from '../../../../utils.js';
12
-
13
- const types = { point: 'ST_Point' /* ,ST_MultiPoint */, polygon: 'ST_Polygon,ST_MultiPolygon' };
14
- const hourMs = 3.6e+6;
15
-
16
- export default async function geojson(req, reply) {
17
-
18
- const {
19
- pg, query = {}, funcs = {}, log,
20
- } = req;
21
-
22
- const { filter, widget, sql, type, nocache, id, dashboard, geom = 'geom', pointZoom = 0 } = query;
23
-
24
- if (!widget && !dashboard) {
25
- return { message: 'not enough params: widget', status: 400 };
26
- }
27
-
28
- const data = await getWidget({ dashboard, widget });
29
- if (data.status) return data;
30
-
31
- const hash = [pointZoom, filter].filter((el) => el).join();
32
-
33
- const root = funcs.getFolder(req);
34
- const file = path.join(root, `/map/geojson/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}.geojson`);
35
-
36
- if (existsSync(file)) {
37
- const timeNow = Date.now();
38
- const stats = await stat(file);
39
- const birthTime = new Date(stats.birthtime).getTime();
40
- if (!(birthTime - timeNow > (hourMs * 24)) && !nocache) {
41
- const res = JSON.parse(await readFile(file, 'utf-8') || {});
42
- return res;
43
- }
44
- }
45
-
46
- try {
47
- const pkey = pg.pk?.[data?.table];
48
- if (!pkey) {
49
- return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`, status: 400 };
50
- }
51
-
52
- // data param
53
- const { table, where = '1=1', xName, x } = normalizeData(data, query);
54
-
55
- if (!xName && !x) {
56
- return { message: `invalid ${widget ? 'widget' : 'dashboard'}: x axis column not specified`, status: 400 };
57
- }
58
-
59
- // get sql
60
- const filterQ = filter ? await funcs.getFilterSQL({ pg, table, filter, query }) : undefined;
61
- const q = `select "${pkey}", "${xName || x}", /* st_asgeojson(geom)::json as */ ${geom} as geom from ${filterQ ? `(${filterQ})` : table} q where ${where}`;
62
-
63
- if (sql === '1') return q;
64
-
65
- const { st_geometrytype: geomType = 'point' } = await pg.query(`select st_geometrytype(${geom}), count(*) from ${table}
66
- where ${where} group by st_geometrytype(${geom})`).then((res) => res.rows?.[0] || {});
67
-
68
- const q1 = `SELECT 'FeatureCollection' As type, json_agg(f) As features FROM (
69
- SELECT 'Feature' As type, row_number() over() as id,
70
- st_asgeojson(st_force2d(${query.srid
71
- ? `st_transform(${type === 'centroid' ? `st_centroid(${geom})` : geom},${query.srid})`
72
- : `${type === 'centroid' || query.point || query.centroid ? `st_centroid(${geom})` : geom}`}), 6, 0)::json as geometry,
73
- (select row_to_json(tc) from (select ${'' ? `${''} as status, ` : ''}
74
- ${xName ? `${xName},` : ''}
75
- ${data.style?.colorAttr ? `${data.style.colorAttr},` : ''}
76
- ${pkey} as id,(select file_path from crm.files
77
- where entity_id=q.${pkey}::text and file_status <>'3' and ext in ('png','jpg') limit 1) as image
78
- )tc) as properties
79
- from (${q})q where ${id && pkey ? ` ${pkey} = '${id}' and ` : ''} ${geom} is not null
80
- ${data.query ? ` and ${data.query}` : ''}
81
- ${query.extent ? `and ${geom} && 'BOX(${query.extent.split(',').reduce((p, el, i) => p + el + (i % 2 ? ',' : ' '), '')})'::box2d` : ''}
82
- ${types[type] ? ` and ST_GeometryType(${geom}) = any ('{ ${types[type]} }') ` : ''}
83
- limit ${geomType?.toLowerCase()?.includes('point') ? '15000' : '2500'})f`;
84
-
85
- if (sql === '2') return q1;
86
-
87
- // auto Index
88
- funcs.autoIndex({ table, columns: [xName] });
89
-
90
- const res = await pg.query(q1).then((res) => res.rows?.[0] || {});
91
-
92
- await mkdir(path.dirname(file), { recursive: true });
93
- await writeFile(file, JSON.stringify(res));
94
-
95
- return res;
96
- }
97
- catch (err) {
98
- log.error('bi/geojson', { error: err.toString(), query });
99
- return { error: err.toString(), status: 500 };
100
- }
101
- }
1
+ import path from 'path';
2
+ import { createHash } from 'crypto';
3
+ import { writeFile, mkdir, readFile, stat } from 'fs/promises';
4
+ import { existsSync, /* readdirSync, */ readFileSync } from 'fs';
5
+
6
+
7
+ import { getFolder, getFilterSQL, autoIndex } from '@opengis/fastify-table/utils.js';
8
+
9
+ import normalizeData from '../../data/controllers/util/normalizeData.js';
10
+
11
+ import { getWidget } from '../../../../utils.js';
12
+
13
+ const types = {
14
+ point: 'ST_Point' /* ,ST_MultiPoint */,
15
+ polygon: 'ST_Polygon,ST_MultiPolygon',
16
+ };
17
+ const hourMs = 3.6e6;
18
+
19
+ export default async function geojson(req, reply) {
20
+ const { pg, query = {}, funcs = {}, log } = req;
21
+
22
+ const {
23
+ filter,
24
+ widget,
25
+ sql,
26
+ type,
27
+ nocache,
28
+ id,
29
+ dashboard,
30
+ geom = 'geom',
31
+ pointZoom = 0,
32
+ } = query;
33
+
34
+ if (!widget && !dashboard) {
35
+ return { message: 'not enough params: widget', status: 400 };
36
+ }
37
+
38
+ const data = await getWidget({ dashboard, widget });
39
+ if (data.status) return data;
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/geojson/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}.geojson`
47
+ );
48
+
49
+ if (existsSync(file)) {
50
+ const timeNow = Date.now();
51
+ const stats = await stat(file);
52
+ const birthTime = new Date(stats.birthtime).getTime();
53
+ if (!(birthTime - timeNow > hourMs * 24) && !nocache) {
54
+ const res = JSON.parse((await readFile(file, 'utf-8')) || {});
55
+ return res;
56
+ }
57
+ }
58
+
59
+ try {
60
+ const pkey = pg.pk?.[data?.table];
61
+ if (!pkey) {
62
+ return {
63
+ message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`,
64
+ status: 400,
65
+ };
66
+ }
67
+
68
+ // data param
69
+ const { table, where = '1=1', xName, x } = normalizeData(data, query);
70
+
71
+ if (!xName && !x) {
72
+ return {
73
+ message: `invalid ${widget ? 'widget' : 'dashboard'}: x axis column not specified`,
74
+ status: 400,
75
+ };
76
+ }
77
+
78
+ // get sql
79
+ const filterQ = filter
80
+ ? await getFilterSQL({ pg, table, filter, query })
81
+ : undefined;
82
+ const q = `select "${pkey}", "${xName || x}", /* st_asgeojson(geom)::json as */ ${geom} as geom from ${filterQ ? `(${filterQ})` : table} q where ${where}`;
83
+
84
+ if (sql === '1') return q;
85
+
86
+ const { st_geometrytype: geomType = 'point' } = await pg
87
+ .query(
88
+ `select st_geometrytype(${geom}), count(*) from ${table}
89
+ where ${where} group by st_geometrytype(${geom})`
90
+ )
91
+ .then((res) => res.rows?.[0] || {});
92
+
93
+ const q1 = `SELECT 'FeatureCollection' As type, json_agg(f) As features FROM (
94
+ SELECT 'Feature' As type, row_number() over() as id,
95
+ st_asgeojson(st_force2d(${
96
+ query.srid
97
+ ? `st_transform(${type === 'centroid' ? `st_centroid(${geom})` : geom},${query.srid})`
98
+ : `${type === 'centroid' || query.point || query.centroid ? `st_centroid(${geom})` : geom}`
99
+ }), 6, 0)::json as geometry,
100
+ (select row_to_json(tc) from (select ${'' ? `${''} as status, ` : ''}
101
+ ${xName ? `${xName},` : ''}
102
+ ${data.style?.colorAttr ? `${data.style.colorAttr},` : ''}
103
+ ${pkey} as id,(select file_path from crm.files
104
+ where entity_id=q.${pkey}::text and file_status <>'3' and ext in ('png','jpg') limit 1) as image
105
+ )tc) as properties
106
+ from (${q})q where ${id && pkey ? ` ${pkey} = '${id}' and ` : ''} ${geom} is not null
107
+ ${data.query ? ` and ${data.query}` : ''}
108
+ ${query.extent ? `and ${geom} && 'BOX(${query.extent.split(',').reduce((p, el, i) => p + el + (i % 2 ? ',' : ' '), '')})'::box2d` : ''}
109
+ ${types[type] ? ` and ST_GeometryType(${geom}) = any ('{ ${types[type]} }') ` : ''}
110
+ limit ${geomType?.toLowerCase()?.includes('point') ? '15000' : '2500'})f`;
111
+
112
+ if (sql === '2') return q1;
113
+
114
+ // auto Index
115
+ autoIndex({ table, columns: [xName] });
116
+
117
+ const res = await pg.query(q1).then((res) => res.rows?.[0] || {});
118
+
119
+ await mkdir(path.dirname(file), { recursive: true });
120
+ await writeFile(file, JSON.stringify(res));
121
+
122
+ return res;
123
+ } catch (err) {
124
+ log.error('bi/geojson', { error: err.toString(), query });
125
+ return { error: err.toString(), status: 500 };
126
+ }
127
+ }