@opengis/gis 0.1.24 → 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.
- package/dist/import-file.cjs +139 -139
- package/dist/import-file.css +1 -1
- package/dist/import-file.js +12627 -11587
- package/module/gis/card/gis.maps.table/index.yml +3 -0
- package/module/gis/card/gis.metadata.table/index.yml +3 -0
- package/module/gis/card/gis.rasters.table/index.yml +3 -0
- package/module/gis/card/gis.registers.table/index.yml +3 -0
- package/module/gis/card/gis.services.table/index.yml +3 -0
- package/module/gis/table/gis.metadata.table.json +70 -70
- package/package.json +6 -5
- package/server/helpers/core/badge.js +17 -0
- package/server/helpers/core/buttonFilePreview.js +13 -0
- package/server/helpers/core/buttonHelper.js +21 -0
- package/server/helpers/core/coalesce.js +8 -0
- package/server/helpers/core/select.js +48 -0
- package/server/helpers/core/token.js +19 -0
- package/server/helpers/index.js +43 -0
- package/server/helpers/list/buttonHelper.js +21 -0
- package/server/helpers/list/descriptionList.js +46 -0
- package/server/helpers/list/tableList.js +86 -0
- package/server/helpers/list/utils/button.js +6 -0
- package/server/helpers/list/utils/buttonDel.js +13 -0
- package/server/helpers/list/utils/buttonEdit.js +15 -0
- package/server/helpers/temp/contentList.js +58 -0
- package/server/helpers/temp/ifCond.js +101 -0
- package/server/helpers/utils/button.js +6 -0
- package/server/helpers/utils/buttonAdd.js +7 -0
- package/server/helpers/utils/buttonDel.js +26 -0
- package/server/helpers/utils/buttonDownload.js +3 -0
- package/server/helpers/utils/buttonEdit.js +19 -0
- package/server/helpers/utils/buttonPreview.js +3 -0
- package/server/helpers/utils/mdToHTML.js +17 -0
- package/server/helpers/utils/paddingNumber.js +4 -0
- package/server/plugins/vite.js +12 -3
- package/server/routes/gis/index.mjs +3 -0
- package/server/routes/gis/registers/funcs/content.type.js +10 -0
- package/server/routes/gis/registers/funcs/get.info.js +89 -0
- 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,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
|
+
};
|
package/server/plugins/vite.js
CHANGED
|
@@ -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
|
+
}
|