@opengis/gis 0.1.25 → 0.1.26

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 (38) hide show
  1. package/dist/import-file.cjs +138 -138
  2. package/dist/import-file.css +1 -1
  3. package/dist/import-file.js +13066 -11971
  4. package/module/gis/card/gis.maps.table/index.yml +3 -0
  5. package/module/gis/card/gis.metadata.table/index.yml +3 -0
  6. package/module/gis/card/gis.rasters.table/index.yml +3 -0
  7. package/module/gis/card/gis.registers.table/index.yml +3 -0
  8. package/module/gis/card/gis.services.table/index.yml +3 -0
  9. package/module/gis/table/gis.metadata.table.json +70 -70
  10. package/package.json +6 -4
  11. package/server/helpers/core/badge.js +17 -0
  12. package/server/helpers/core/buttonFilePreview.js +13 -0
  13. package/server/helpers/core/buttonHelper.js +21 -0
  14. package/server/helpers/core/coalesce.js +8 -0
  15. package/server/helpers/core/select.js +48 -0
  16. package/server/helpers/core/token.js +19 -0
  17. package/server/helpers/index.js +43 -0
  18. package/server/helpers/list/buttonHelper.js +21 -0
  19. package/server/helpers/list/descriptionList.js +46 -0
  20. package/server/helpers/list/tableList.js +86 -0
  21. package/server/helpers/list/utils/button.js +6 -0
  22. package/server/helpers/list/utils/buttonDel.js +13 -0
  23. package/server/helpers/list/utils/buttonEdit.js +15 -0
  24. package/server/helpers/temp/contentList.js +58 -0
  25. package/server/helpers/temp/ifCond.js +101 -0
  26. package/server/helpers/utils/button.js +6 -0
  27. package/server/helpers/utils/buttonAdd.js +7 -0
  28. package/server/helpers/utils/buttonDel.js +26 -0
  29. package/server/helpers/utils/buttonDownload.js +3 -0
  30. package/server/helpers/utils/buttonEdit.js +19 -0
  31. package/server/helpers/utils/buttonPreview.js +3 -0
  32. package/server/helpers/utils/mdToHTML.js +17 -0
  33. package/server/helpers/utils/paddingNumber.js +4 -0
  34. package/server/plugins/vite.js +12 -3
  35. package/server/routes/gis/index.mjs +3 -0
  36. package/server/routes/gis/registers/funcs/content.type.js +10 -0
  37. package/server/routes/gis/registers/funcs/get.info.js +89 -0
  38. package/server/routes/gis/registers/gis.export.js +149 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Перетинає два масиви
3
+ *
4
+ * @example
5
+ * // returns [1, 4, 5, 6]
6
+ * intersect([1,2,3,4,5,6],[1,4,5,6,7,8,9,11])
7
+ * @param {Array} a
8
+ * @param {Array} b
9
+ * @returns {Array} Returns new intersect array
10
+ */
11
+ function intersect(a, b) {
12
+ let aN = a; let bN = b;
13
+ if (b.length > a.length) {
14
+ [aN, bN] = [bN, aN];
15
+ }
16
+ return aN.filter((e) => bN.includes(e));
17
+ }
18
+
19
+ /**
20
+ * Створення шаблона або його частини внаслідок перевірки значення із веб-запиту та заздалегідь прописаного значення.
21
+ * Дозволяє змінювати наповнення сторінки через ряд перевірок. Є можливість внесення додаткової умови - що робити, коли умова не виконується.
22
+ *
23
+ * @summary Перевірка двох значень та виконання коду при виконанні умови, а також у всіх інших випадках.
24
+ * @priority 5
25
+ * @alias ifCond
26
+ * @type helper
27
+ * @tag condition
28
+ * @example
29
+ * {{#ifCond @root.req.domain 'in' 'help.softpro.ua,123'}} {{select user.uid data="get_full_uid"}} {{^}} Умова не виконана {{/ifCond}}
30
+ * @example
31
+ * {{#ifCond "1234567890" 'in' @root.user.group_list}} 1=1 {{^}} uid='{{uid}}' {{/ifCond}}
32
+ * @example
33
+ * {{#ifCond 'debug' 'in' @root.setting.core.setting}}Умова виконана{{^}}Не виконана умова{{/ifCond}}
34
+ * @param {Array} args Параметри для значень і умов
35
+ * @param {Array} args[0]] Перше значення
36
+ * @param {Array} args[1]] Оператор
37
+ * @param {Array} args[2]] Друге значення
38
+ * @returns {String} Returns HTML
39
+ */
40
+ export default function ifCond(v1, operator, v2, options) {
41
+ const __obj = this;
42
+
43
+ switch (operator) {
44
+ case '==':
45
+ return (v1 == v2) ? options.fn(__obj) : options.inverse(__obj);
46
+ case '!=':
47
+ return (v1 != v2) ? options.fn(__obj) : options.inverse(__obj);
48
+ case '===':
49
+ return (v1 === v2) ? options.fn(__obj) : options.inverse(__obj);
50
+ case '!==':
51
+ return (v1 !== v2) ? options.fn(__obj) : options.inverse(__obj);
52
+ case '&&':
53
+ return (v1 && v2) ? options.fn(__obj) : options.inverse(__obj);
54
+ case '||':
55
+ return (v1 || v2) ? options.fn(__obj) : options.inverse(__obj);
56
+ case '<':
57
+ return (v1 < v2) ? options.fn(__obj) : options.inverse(__obj);
58
+ case '<=':
59
+ return (v1 <= v2) ? options.fn(__obj) : options.inverse(__obj);
60
+ case '>':
61
+ return (v1 > v2) ? options.fn(__obj) : options.inverse(__obj);
62
+ case '>=':
63
+ return (v1 >= v2) ? options.fn(__obj) : options.inverse(__obj);
64
+ case '&':
65
+ return intersect(v1, v2).length !== 0
66
+ ? options.fn(__obj)
67
+ : options.inverse(__obj);
68
+ case '!~':
69
+ return (v1 || '').indexOf(v2) === -1
70
+ ? options.fn(__obj)
71
+ : options.inverse(__obj);
72
+ case '~':
73
+ return (v1 || '').indexOf(v2) !== -1
74
+ ? options.fn(__obj)
75
+ : options.inverse(__obj);
76
+ case 'period':
77
+ return (new Date(v1) < new Date() && new Date(v2) > new Date())
78
+ ? options.fn(__obj)
79
+ : options.inverse(__obj);
80
+ case 'in': {
81
+ if (typeof v2 === 'string') v2 = v2.split(',').map(item => item.trim());
82
+
83
+ if (Array.isArray(v1)) {
84
+ return v1.some((value) => v2.includes(value.toString()))
85
+ ? options.fn(__obj)
86
+ : options.inverse(__obj);
87
+ }
88
+ return v2.includes(v1?.toString())
89
+ ? options.fn(__obj)
90
+ : options.inverse(__obj);
91
+ }
92
+ case 'not in': {
93
+ if (typeof v2 === 'string') v2 = v2.split(',').map(item => item.trim());
94
+ return !v2.includes(v1?.toString())
95
+ ? options.fn(__obj)
96
+ : options.inverse(__obj);
97
+ }
98
+ default:
99
+ return options.inverse(__obj);
100
+ }
101
+ }
@@ -0,0 +1,6 @@
1
+
2
+
3
+ export default function button(token, title) {
4
+ return `<button onclick="window.v3plugin.$form({ token: '${token}' })"
5
+ class="inline-flex items-center px-2 py-1 text-sm font-medium text-white duration-300 bg-blue-600 border border-transparent rounded-lg gap-x-2 hover:bg-blue-700 hover:text-white">${title || 'Редагувати'}</button>`;
6
+ }
@@ -0,0 +1,7 @@
1
+
2
+
3
+ const newColor = 'blue'
4
+ export default function button(token, title) {
5
+ return `<button onclick="window.v3plugin.$form({ token: '${token}' })"
6
+ class="px-2 py-1 inline-flex border-solid justify-center items-center gap-2 rounded-md font-semibold focus:outline-none text-sm transition-all border border-transparent hover:text-white ring-offset-white bg-${newColor}-100 text-${newColor}-500 hover:bg-${newColor}-500 focus:ring-${newColor}-500">${title || 'Додати'}</button>`;
7
+ }
@@ -0,0 +1,26 @@
1
+ const newColor = 'red';
2
+
3
+ export default function button(token, title, icon) {
4
+ const apiCall = `window.v3plugin.$api({
5
+ api: '/api/table/${token}',
6
+ method: 'delete',
7
+ confirm: {
8
+ title: 'Підтвердити операцію',
9
+ text: 'Ви впевнені що хочете вилучити запис?',
10
+ cancel: 'Скасувати',
11
+ confirm: 'Виконати'
12
+ }
13
+ })`;
14
+
15
+ const buttonClass = icon
16
+ ? `size-8 inline-flex justify-center items-center gap-x-2 rounded-e-lg border border-stone-200 bg-${newColor}-100 text-stone-800 shadow-sm hover:bg-${newColor}-200 disabled:opacity-50 disabled:pointer-events-none focus:outline-none focus:bg-stone-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700`
17
+ : `px-2 py-1 inline-flex border-solid justify-center items-center gap-2 rounded-md font-semibold focus:outline-none text-sm transition-all border border-transparent hover:text-white ring-offset-white bg-${newColor}-100 text-${newColor}-500 hover:bg-${newColor}-500 focus:ring-${newColor}-500`;
18
+
19
+ const buttonContent = icon
20
+ ? `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="${newColor}" class="size-3.5">
21
+ <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/>
22
+ </svg>`
23
+ : `${title || 'Вилучити'}`;
24
+
25
+ return `<button onclick="${apiCall}" class="${buttonClass}">${buttonContent}</button>`;
26
+ }
@@ -0,0 +1,3 @@
1
+ export default function buttonDownload(filepath) {
2
+ return `<a download=1 class="flex items-center gap-x-2 border py-2 px-3 shadow rounded-xl cursor-pointer w-[42px] y-[34px]" href="${filepath}" target="_blank"><img src="https://cdn.softpro.ua/assets/file-up.svg" alt="generate"></a>`;
3
+ }
@@ -0,0 +1,19 @@
1
+
2
+ const newColor = 'blue';
3
+ export default function button(token, title, icon) {
4
+ const formCall = `window.v3plugin.$form({ token: '${token}' })`;
5
+
6
+ const buttonClass = icon
7
+ ? `size-8 inline-flex justify-center items-center gap-x-2 font-medium rounded-s-lg border border-stone-200 bg-${newColor}-100 text-stone-800 shadow-sm hover:bg-${newColor}-200 disabled:opacity-50 disabled:pointer-events-none focus:outline-none focus:bg-${newColor}-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 `
8
+ : `px-2 py-1 inline-flex border-solid justify-center items-center gap-2 rounded-md font-semibold focus:outline-none text-sm transition-all border border-transparent hover:text-white ring-offset-white bg-${newColor}-100 text-${newColor}-500 hover:bg-${newColor}-500 focus:ring-${newColor}-500`;
9
+
10
+ const buttonContent = icon
11
+ ? `<svg class="shrink-0 size-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="${newColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
12
+ <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path>
13
+ <path d="m15 5 4 4"></path>
14
+ </svg>`
15
+ : `${title || 'Редагувати'}`;
16
+
17
+ return `<button onclick="${formCall}" class="${buttonClass}">${buttonContent}</button>`;
18
+
19
+ }
@@ -0,0 +1,3 @@
1
+ export default function buttonPreview(filepath) {
2
+ return `<a class="flex items-center gap-x-2 border py-2 px-3 shadow rounded-xl cursor-pointer w-[42px] y-[34px]" href="${filepath}" target="_blank"><img src="https://cdn.softpro.ua/assets/eye.svg" alt="eye"></a>`;
3
+ }
@@ -0,0 +1,17 @@
1
+ import md from 'markdown-it';
2
+
3
+ const md1 = md({ html: true });
4
+
5
+ /**
6
+ * Перетворення з файла readme.md до формату HTML.
7
+ * Потрабно вставити в хелпер шлях до файла або текст readme.md і за допомогою бібліотеки markdown-it перетвориться в HTML.
8
+
9
+ * @returns {String} Returns HTML
10
+ */
11
+ export default function mdToHTML(data, options) {
12
+ // auto detect HTML or MD
13
+ // const result = md().render(data);
14
+ if (!data) return 'empty data';
15
+ const result = md1.render(data);
16
+ return result;
17
+ };
@@ -0,0 +1,4 @@
1
+ export default function (num) {
2
+ const padding = arguments.length === 3 ? arguments[1] : 6;
3
+ return num.toLocaleString('en', { minimumIntegerDigits: padding, useGrouping: false })
4
+ }
@@ -1,12 +1,13 @@
1
- import fs from 'fs';
2
- import path from 'path';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { createServer } from 'vite';
4
4
 
5
+ import { config } from '@opengis/fastify-table/utils.js';
6
+
5
7
  const isProduction = process.env.NODE_ENV === 'production';
6
8
 
7
9
  console.log({ isProduction });
8
10
 
9
-
10
11
  async function plugin(fastify, opts) {
11
12
 
12
13
  const viteServer = !isProduction ? await createServer({
@@ -28,6 +29,10 @@ async function plugin(fastify, opts) {
28
29
 
29
30
  // this is middleware for vite's dev server
30
31
  fastify.addHook('onRequest', async (request, reply) => {
32
+ const { user } = request.session?.passport || {};
33
+ if (!user && config.pg && !config.auth?.disable) {
34
+ return reply.redirect('/login');
35
+ }
31
36
  const next = () => new Promise((resolve) => {
32
37
  viteServer.middlewares(request.raw, reply.raw, () => resolve());
33
38
  });
@@ -57,6 +62,10 @@ async function plugin(fastify, opts) {
57
62
  fastify.get('/public/*', staticFile);
58
63
 
59
64
  fastify.get('*', async (req, reply) => {
65
+ const { user } = req.session?.passport || {};
66
+ if (!user && config.pg && !config.auth?.disable) {
67
+ return reply.redirect('/login');
68
+ }
60
69
  if (!isProduction) return null; // admin vite
61
70
  const stream = fs.createReadStream('admin/dist/index.html');
62
71
  return reply.type('text/html').send(stream);
@@ -5,6 +5,7 @@ import getLayerGeom from './services/get.layer.geom.js';
5
5
  import gisRegistry from './registers/gis.registry.js';
6
6
  import gisRegistryList from './registers/gis.registry.list.js';
7
7
  import mapRegistry from './registers/map.registry.js';
8
+ import gisExport from './registers/gis.export.js';
8
9
 
9
10
  export default async function route(app) {
10
11
  app.put('/insert-columns/:token', insertColumns);
@@ -17,4 +18,6 @@ export default async function route(app) {
17
18
  app.get('/xml/:id', { config: { policy: ['public'] } }, metadataXML);
18
19
 
19
20
  app.get('/get-layer-geom/:id', { config: { policy: ['public'] } }, getLayerGeom);
21
+
22
+ app.get('/gis-export/:type/:slug', { config: { policy: ['public'] } }, gisExport);
20
23
  }
@@ -0,0 +1,10 @@
1
+ const ContentType = {
2
+ xml: 'application/xml',
3
+ shp: 'application/zip',
4
+ csv: 'text/csv',
5
+ geojson: 'application/vnd.geo+json',
6
+ json: 'application/json',
7
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
8
+ };
9
+
10
+ export default ContentType;
@@ -0,0 +1,89 @@
1
+ import { pgClients } from '@opengis/fastify-table/utils.js';
2
+ import { attachClassifiers } from '../../../gis/registers/funcs/classifiers.js';
3
+ const pg = pgClients.client;
4
+
5
+ export default async function getInfo({ finalInfo, format }, reply) {
6
+ if (!finalInfo) return { error: 'No data found' };
7
+
8
+ const table = finalInfo?.table_name;
9
+ const queryCondition = finalInfo?.query || '1=1';
10
+ const columns = finalInfo?.columns || finalInfo?.attributes || '[]';
11
+ const exportable = columns.filter(c => c.is_export);
12
+ const selectNames = exportable.map(c => `"${c.name}"`);
13
+ const selectSQL = `
14
+ SELECT ${selectNames?.length ? selectNames.join(', ') : ''} ${format === 'geojson' || format === 'shp'? `, ST_AsGeoJSON(geom)::json as geom` : ``}
15
+ FROM ${table}
16
+ WHERE ${queryCondition} ${format === 'geojson' || format === 'shp'? `and geom is not null` : ``}
17
+ `;
18
+
19
+ if (format === 'shp') {
20
+ const { rows: geomTypeRows } = await pg.query(`
21
+ SELECT DISTINCT ST_GeometryType(geom) AS geom_type
22
+ FROM ${table}
23
+ WHERE geom IS NOT NULL
24
+ `);
25
+ const geometryTypes = geomTypeRows.map(row => row.geom_type);
26
+
27
+ const sqlList = geometryTypes.map(type => {
28
+ return {
29
+ type,
30
+ query: `
31
+ SELECT ${selectNames.join(', ')}, ST_AsGeoJSON(geom)::json as geom
32
+ FROM ${table}
33
+ WHERE ${queryCondition} AND ST_GeometryType(geom) = '${type}' AND geom IS NOT NULL
34
+ `,
35
+ };
36
+ });
37
+
38
+ const { rows: [{ srid }] } = await pg.query(`
39
+ SELECT ST_SRID(geom) AS srid
40
+ FROM ${table}
41
+ WHERE geom IS NOT NULL
42
+ LIMIT 1;
43
+ `);
44
+
45
+ return {
46
+ columns: exportable,
47
+ table,
48
+ srid: srid || 4326,
49
+ geomTypes: geometryTypes,
50
+ sqlList
51
+ };
52
+ }
53
+
54
+ const { rows } = await pg.query(selectSQL);
55
+ if (!rows?.length) {
56
+ return reply.status(404).send({ message: 'Немає даних, які можна експортувати', status: 404 });
57
+ }
58
+
59
+ const classifiers = exportable
60
+ .filter(col => col.data && ["select", "badge", "tags"].includes(col.format))
61
+ .map(col => ({ name: col.name, classifier: col.data }));
62
+
63
+ const rowsWithClassifiers = await attachClassifiers(rows, classifiers);
64
+
65
+ const simplifiedRows = rowsWithClassifiers.map(row => {
66
+ const newRow = { ...row };
67
+
68
+ for (const key of Object.keys(newRow)) {
69
+ if (key.endsWith('_data') && typeof newRow[key]?.text === 'string') {
70
+ const baseKey = key.replace(/_data$/, '');
71
+ newRow[baseKey] = newRow[key].text;
72
+ }
73
+
74
+ if (key.endsWith('_text') && typeof newRow[key] === 'string') {
75
+ const baseKey = key.replace(/_text$/, '');
76
+ newRow[baseKey] = newRow[key];
77
+ }
78
+ }
79
+
80
+ return newRow;
81
+ });
82
+
83
+ return {
84
+ rows: simplifiedRows,
85
+ data: rows,
86
+ columns: exportable,
87
+ table
88
+ };
89
+ }
@@ -0,0 +1,149 @@
1
+ import { createReadStream } from 'fs';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import path from 'path';
4
+ import { config, getMeta, getFolder } from '@opengis/fastify-table/utils.js';
5
+ import { grpc } from '@opengis/fastify-file/utils.js';
6
+ import convertJSONToCSV from '@opengis/fastify-file/server/routes/file/controllers/utils/convertJSONToCSV.js';
7
+ import getInfo from './funcs/get.info.js';
8
+ import ContentType from './funcs/content.type.js';
9
+ const { jsonToXls, geojsonToShp } = grpc();
10
+ const rootDir = getFolder(config, 'local');
11
+
12
+ /**
13
+ * Експорт даних реєстру
14
+ * @param {Object} query - Об'єкт запиту з форматом.
15
+ * @param {Object} params - Об'єкт параметрів з slug та type.
16
+ * @param {Function} reply
17
+ * @returns {boolean} File
18
+ */
19
+
20
+ export default async function gisExport({ pg, query = {}, params = {} }, reply) {
21
+ const { format } = query;
22
+ const { slug, type } = params;
23
+ if (!['csv', 'xlsx', 'shp', 'geojson'].includes(format)) {
24
+ return reply.code(401).send({ message: 'Param format is invalid. Allowed: csv, xlsx, shp, geojson', status: 404 });
25
+ }
26
+ if (!slug) return reply.code(401).send({ message: 'slug is not defined', status: 404 });
27
+ if (!type) return reply.code(401).send({ message: 'type is not defined', status: 404 });
28
+
29
+ let registerInfo;
30
+ let serviceInfo;
31
+
32
+ if (type === 'register') {
33
+ registerInfo = await pg.query(
34
+ `SELECT table_name, columns, query
35
+ FROM gis.registers
36
+ WHERE register_key = $1`,
37
+ [slug]
38
+ );
39
+ }
40
+ if (type === 'service') {
41
+ serviceInfo = await pg.query(
42
+ `SELECT source_path as table_name, attributes as columns, query
43
+ FROM gis.services
44
+ WHERE service_key = $1`,
45
+ [slug]
46
+ );
47
+ }
48
+ const finalInfo = registerInfo?.rows[0] || serviceInfo?.rows[0];
49
+
50
+ const exportable = finalInfo?.columns?.filter(c => c.is_export);
51
+ const colmodel = exportable.map(col => ({
52
+ name: col.name,
53
+ title: col.ua || col.name
54
+ }));
55
+ const { rows, table, srid, sqlList } = await getInfo({ finalInfo, format }, reply);
56
+ let filePath = ['csv', 'geojson', 'xlsx'].includes(format) ? path.join(rootDir, `/files/tmp/export_${table}_${Date.now()}.${format}`) : path.join(rootDir, `/files/tmp/export_${table}_${Date.now()}.zip`);
57
+ const ext = path.extname(filePath);
58
+ const filePathJSON = ['csv', 'geojson', 'shp'].includes(format) ? filePath.replace(ext, '.json') : filePath;
59
+ let exportedZipPaths = [];
60
+
61
+ const meta = await getMeta({ pg, table: table });
62
+ if ((format === 'geojson' || format === 'shp') && !meta?.geom) {
63
+ return reply.status(400).send('Ця таблиця не містить полів геометрії. Виберіть тип, який не потребує геометрії для вивантаження');
64
+ }
65
+
66
+ if (format === 'xlsx') {
67
+ if (!jsonToXls) {
68
+ const message = 'Сервіс конвертації jsonToXls тимчасово недоступний. Спробуйте експортувати дані у іншому форматі...';
69
+ const error = new Error(message);
70
+ error.status = 503;
71
+
72
+ throw error;
73
+ }
74
+ const { result } = await jsonToXls({
75
+ json: JSON.stringify(rows),
76
+ colmodel: JSON.stringify(colmodel),
77
+ });
78
+
79
+ await mkdir(path.dirname(filePath), { recursive: true });
80
+ await writeFile(filePath, result, 'base64');
81
+ }
82
+
83
+ if (format === 'csv') {
84
+ await mkdir(path.dirname(filePathJSON), { recursive: true });
85
+ await writeFile(filePathJSON, JSON.stringify(rows));
86
+
87
+ await convertJSONToCSV({
88
+ filePath: filePathJSON, colmodel, columnList: colmodel.map(c => c.name),
89
+ });
90
+ }
91
+
92
+ if (['shp', 'geojson'].includes(format)) {
93
+ if (format === 'geojson') {
94
+ const geojson = {
95
+ type: 'FeatureCollection',
96
+ features: rows.map((row) => ({
97
+ type: 'Feature',
98
+ geometry: row.geom,
99
+ properties: Object.fromEntries(Object.entries(row).filter(([key]) => key !== 'geom')),
100
+ })),
101
+ };
102
+ filePath = filePathJSON.replace('.json', '.geojson');
103
+ await mkdir(path.dirname(filePath), { recursive: true });
104
+ await writeFile(filePath, JSON.stringify(geojson));
105
+ }
106
+
107
+ if (format === 'shp') {
108
+ const proj = `EPSG:${srid || 4326}`;
109
+ for (const { type, query } of sqlList) {
110
+ const { rows } = await pg.query(query);
111
+ if (!rows.length) continue;
112
+
113
+ const geojson = {
114
+ type: 'FeatureCollection',
115
+ features: rows.map(row => ({
116
+ type: 'Feature',
117
+ geometry: row.geom,
118
+ properties: Object.fromEntries(Object.entries(row).filter(([k]) => k !== 'geom')),
119
+ })),
120
+ };
121
+ const geomShort = type.toLowerCase().replace('st_', '');
122
+ const typedPath = filePath.replace('.zip', `_${geomShort}.zip`);
123
+
124
+ const filePathTyped = filePath.replace('.zip', `_${type.toLowerCase().replace('st_', '')}.zip`);
125
+ const { result: shpZipBuffer } = await geojsonToShp({
126
+ geojson: JSON.stringify(geojson),
127
+ proj,
128
+ });
129
+
130
+ await mkdir(path.dirname(filePathTyped), { recursive: true });
131
+ await writeFile(filePathTyped, Buffer.from(shpZipBuffer, 'base64'));
132
+
133
+ exportedZipPaths.push(typedPath);
134
+ }
135
+ }
136
+ }
137
+
138
+ let exportFilePath = filePath;
139
+
140
+ if (format === 'shp' && exportedZipPaths?.length > 0) {
141
+ exportFilePath = exportedZipPaths[0];
142
+ }
143
+
144
+ reply.header('Content-Disposition', `attachment; filename=${encodeURIComponent(path.basename(exportFilePath))}`);
145
+ reply.header('Content-Type', ContentType[format]);
146
+
147
+ const fileStream = createReadStream(exportFilePath);
148
+ return reply.send(fileStream);
149
+ }