@opengis/cms 0.0.57 → 0.0.58
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/README.md +131 -131
- package/dist/AddNewItemInTree-05PSSEFi.js +76 -0
- package/dist/ArticlesPage-CFjE_cw_.js +298 -0
- package/dist/CollectionsBreadcrumb-BCxeRikP.js +4 -0
- package/dist/CollectionsBreadcrumb.vue_vue_type_script_setup_true_lang-umRzB5mY.js +53 -0
- package/dist/CollectionsPage-DHfPNql6.js +124 -0
- package/dist/{CreateForm-BMOBeP4G.js → CreateForm-5FvT45vH.js} +1 -1
- package/dist/Dashboard-C1eGscNd.js +358 -0
- package/dist/EditCollectionPage-DIr1tdtn.js +187 -0
- package/dist/{EmptyData-DaZt_nAm.js → EmptyData-DxPrSXhV.js} +1 -1
- package/dist/{MenuAddPage-Bf48Z-ah.js → MenuAddPage-D-p3gFgm.js} +40 -35
- package/dist/MenuBody-rN5j4YBu.js +125 -0
- package/dist/MenuItemPage-BoJw885D.js +1027 -0
- package/dist/MenuList-DFEBS0NB.js +172 -0
- package/dist/MenuPage-BCZB_S8j.js +107 -0
- package/dist/MenuWrapper-AZ_8s-zd.js +12 -0
- package/dist/MonacoEditor-Db-3Jc3E.js +4 -0
- package/dist/{UniversalTable.vue_vue_type_script_setup_true_lang-CJGTsd1V.js → UniversalTable-CzqPG-tY.js} +12 -12
- package/dist/{UniversalTablePagination.vue_vue_type_script_setup_true_lang-GYZd_gkA.js → UniversalTablePagination-4gL47A7I.js} +1 -1
- package/dist/VsFormTags-CMjiu9sY.js +114 -0
- package/dist/VsPreview-DwETkOpb.js +63 -0
- package/dist/contentForm-CtMhQTG0.js +489 -0
- package/dist/getField-CpwVE28P.js +179 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.html +29 -29
- package/dist/index.js +72 -71
- package/dist/style.css +1 -1
- package/dist/vs-builder-edit-D-q1o8tF.js +604 -0
- package/dist/vs-builder-monaco-B3Jj0V31.js +33 -0
- package/dist/vs-builder-preview-BH4VAM3a.js +44 -0
- package/dist/vs-form-custom-datatable-BDZo48w3.js +317 -0
- package/dist/vs-form-integer-BZ855R3g.js +61 -0
- package/dist/vs-form-media-select-NY27EaG1.js +837 -0
- package/dist/vs-form-reference-list-Dtv8fJJU.js +1536 -0
- package/dist/vs-form-reletion-link-BhzNQszm.js +34 -0
- package/dist/vs-form-tiptap-DDFQjRjY.js +4 -0
- package/dist/vs-form-tiptap.vue_vue_type_script_setup_true_lang-DGgsqXwg.js +11 -0
- package/dist/vs-richtext-md-C098v_6Q.js +4 -0
- package/dist/vs-richtext-md.vue_vue_type_script_setup_true_lang-Ct8uTV-J.js +14 -0
- package/input-types.json +9 -9
- package/locales/en.json +815 -814
- package/locales/uk.json +813 -812
- package/module/cms/cls/content.status.json +17 -17
- package/module/cms/cls/user_type.json +9 -9
- package/module/cms/form/admin.users.form.json +77 -77
- package/module/cms/select/cms.page_type.sql +1 -1
- package/module/cms/select/news_tag_id.sql +11 -11
- package/module/cms/table/admin.users.table.json +53 -53
- package/module/cms/table/collection.default.table.json +96 -96
- package/module/cms/table/single.default.table.json +116 -116
- package/package.json +68 -68
- package/plugin.js +43 -43
- package/server/app.js +35 -35
- package/server/config.js +4 -4
- package/server/functions/getContent.js +45 -45
- package/server/functions/getDraftKey.js +22 -22
- package/server/functions/getSearchData.js +31 -31
- package/server/functions/getTags.js +30 -30
- package/server/functions/getUser.js +27 -27
- package/server/functions/utils/mock.reply.js +55 -55
- package/server/index.js +22 -22
- package/server/migrations/fixes.sql +129 -129
- package/server/migrations/site.sql +595 -595
- package/server/plugins/adminHook.js +78 -78
- package/server/plugins/hook.js +59 -59
- package/server/plugins/vite.js +75 -75
- package/server/routes/category/controllers/cms.category.delete.js +21 -21
- package/server/routes/category/controllers/cms.category.get.js +17 -17
- package/server/routes/category/controllers/cms.category.list.js +16 -16
- package/server/routes/category/controllers/cms.category.post.js +21 -21
- package/server/routes/category/controllers/cms.category.put.js +23 -23
- package/server/routes/category/index.mjs +22 -22
- package/server/routes/cms/controllers/cmsStat.js +55 -55
- package/server/routes/cms/controllers/cmsSuggest.js +57 -57
- package/server/routes/cms/controllers/deleteContent.js +113 -113
- package/server/routes/cms/controllers/deleteMedia.js +76 -76
- package/server/routes/cms/controllers/downloadMedia.js +84 -84
- package/server/routes/cms/controllers/getContent.js +113 -113
- package/server/routes/cms/controllers/getContentBySlug.js +93 -93
- package/server/routes/cms/controllers/insertContent.js +217 -217
- package/server/routes/cms/controllers/listMedia.js +155 -155
- package/server/routes/cms/controllers/metadataMedia.js +39 -39
- package/server/routes/cms/controllers/properties.get.js +18 -18
- package/server/routes/cms/controllers/properties.post.js +99 -99
- package/server/routes/cms/controllers/searchContent.js +214 -214
- package/server/routes/cms/controllers/translate.js +89 -89
- package/server/routes/cms/controllers/updateContent.js +266 -266
- package/server/routes/cms/controllers/uploadMedia.js +79 -79
- package/server/routes/cms/functions/getSettings.js +48 -48
- package/server/routes/cms/index.mjs +112 -112
- package/server/routes/cms/utils/additionalData.js +35 -35
- package/server/routes/cms/utils/getCollection.js +89 -89
- package/server/routes/cms/utils/getSingle.js +188 -188
- package/server/routes/cms/utils/inputTypes.js +5 -5
- package/server/routes/cms/utils/insertContentLocalization.js +104 -104
- package/server/routes/cms/utils/requestTranslation.js +85 -85
- package/server/routes/cms/utils/updateLocalization.js +47 -47
- package/server/routes/cmsSpace/controllers/deleteSpace.js +25 -25
- package/server/routes/cmsSpace/controllers/getSpaces.js +27 -27
- package/server/routes/cmsSpace/controllers/insertSpace.js +21 -21
- package/server/routes/cmsSpace/controllers/updateSpace.js +23 -23
- package/server/routes/cmsSpace/index.mjs +20 -20
- package/server/routes/contentType/controllers/addContentType.js +160 -160
- package/server/routes/contentType/controllers/contentTypeList.js +54 -54
- package/server/routes/contentType/controllers/delContentType.js +75 -75
- package/server/routes/contentType/controllers/editContentType.js +88 -88
- package/server/routes/contentType/controllers/getContentType.js +65 -65
- package/server/routes/contentType/index.mjs +35 -35
- package/server/routes/contentType/utils/updateContents.js +44 -44
- package/server/routes/contentType/utils/updateCustomContentTable.js +53 -53
- package/server/routes/feedback/controllers/email.list.js +24 -24
- package/server/routes/feedback/controllers/feedback.js +48 -48
- package/server/routes/feedback/controllers/feedback.list.js +37 -37
- package/server/routes/feedback/controllers/news.subscriptions.js +44 -44
- package/server/routes/feedback/index.mjs +71 -71
- package/server/routes/logs/controllers/export.user.logs.js +77 -77
- package/server/routes/logs/controllers/user.logs.js +44 -44
- package/server/routes/logs/index.mjs +9 -9
- package/server/routes/menu/controllers/addMenu.js +37 -37
- package/server/routes/menu/controllers/delMenu.js +31 -31
- package/server/routes/menu/controllers/editMenu.js +41 -41
- package/server/routes/menu/controllers/getMenu.js +24 -24
- package/server/routes/menu/functions/getMenu.js +50 -50
- package/server/routes/menu/index.mjs +13 -13
- package/server/routes/migration/controllers/collectionToCustom.js +137 -137
- package/server/routes/migration/index.mjs +8 -8
- package/server/routes/root.mjs +8 -8
- package/server/routes/tags/controllers/add.tags.js +24 -24
- package/server/routes/tags/controllers/del.tags.js +19 -19
- package/server/routes/tags/controllers/edit.tags.js +25 -25
- package/server/routes/tags/controllers/get.tags.js +15 -15
- package/server/routes/tags/index.mjs +14 -14
- package/server/templates/cls/cms.category_type.json +9 -9
- package/server/templates/cls/cms.content_review_status.json +9 -9
- package/server/templates/cls/cms.content_status.json +9 -9
- package/server/templates/cls/cms.content_type.json +9 -9
- package/server/templates/cls/cms.lang.json +9 -9
- package/server/templates/page/login.html +126 -126
- package/utils.d.ts +52 -52
- package/utils.js +8 -8
- package/dist/ArticlesPage-BcR1hbds.js +0 -286
- package/dist/BuilderPage-CK_osM89.js +0 -386
- package/dist/CollectionsBreadcrumb.vue_vue_type_script_setup_true_lang-CnOe9ORD.js +0 -45
- package/dist/CollectionsPage-JfmrHNR_.js +0 -110
- package/dist/EditCollectionPage-Cw3GQYRe.js +0 -809
- package/dist/MenuItemPage-CXn5HC8j.js +0 -1366
- package/dist/MenuPage-tJZtK46W.js +0 -106
- package/dist/contentForm-B6gHgGkz.js +0 -586
- package/dist/getField-Y5WXnRR0.js +0 -2948
|
@@ -1,156 +1,156 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
4
|
-
|
|
5
|
-
import { config, getFolder, pgClients, getMimeType, getFilterSQL } from '@opengis/fastify-table/utils.js';
|
|
6
|
-
import { createHash } from 'node:crypto';
|
|
7
|
-
|
|
8
|
-
const filterList = [
|
|
9
|
-
{
|
|
10
|
-
name: "created_at",
|
|
11
|
-
type: "Date",
|
|
12
|
-
ua: "Дата створення"
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
name: "updated_at",
|
|
16
|
-
type: "Date",
|
|
17
|
-
ua: "Дата редагування"
|
|
18
|
-
}
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
// path.resolve() converts POSIX paths from getFolder to valid Windows paths (Bun/Node fs require this on Windows)
|
|
22
|
-
const rootDir = path.resolve(getFolder(config, 'local'));
|
|
23
|
-
const dir = '/files';
|
|
24
|
-
|
|
25
|
-
const filesizeCache = {};
|
|
26
|
-
const fileMtimeCache = {};
|
|
27
|
-
|
|
28
|
-
mkdirSync(path.join(rootDir, dir), { recursive: true });
|
|
29
|
-
|
|
30
|
-
export default async function listMedia(req, reply) {
|
|
31
|
-
const { pg = pgClients.client, query = {} } = req;
|
|
32
|
-
const { subdir: subdir1 = '', search } = query;
|
|
33
|
-
|
|
34
|
-
if (!pg.pk?.['site.media']) {
|
|
35
|
-
return reply.status(404).send('table not found');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (typeof subdir1 !== 'string' || subdir1.includes('..')) {
|
|
39
|
-
return reply.status(403).send('invalid params: subdir');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const subdir = subdir1.replace(/\/{2,}/g, '/');
|
|
43
|
-
const relpath = path.join(dir, subdir1).replace(/\\/g, '/');
|
|
44
|
-
const dirpath = path.join(rootDir, relpath).replace(/\\/g, '/');
|
|
45
|
-
|
|
46
|
-
if (!existsSync(dirpath)) {
|
|
47
|
-
return { data: [], relpath, msg: 'directory not exists' };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const isDirectory = (await stat(dirpath)).isDirectory();
|
|
51
|
-
|
|
52
|
-
const allItems = isDirectory ? await readdir(dirpath, { withFileTypes: true, recursive: true }) : [];
|
|
53
|
-
const items = isDirectory ? await readdir(dirpath, { withFileTypes: true }) : [];
|
|
54
|
-
|
|
55
|
-
const { q = '' } = await getFilterSQL({ table: 'site.media', filter: query?.filter, filterList, pg });
|
|
56
|
-
|
|
57
|
-
const rows = await pg.query(
|
|
58
|
-
`select
|
|
59
|
-
media_id as id, filename, filetype, filesize, url, description, alt,
|
|
60
|
-
mime, preview_url, created_at, updated_at, created_by, updated_by
|
|
61
|
-
from site.media
|
|
62
|
-
where ${subdir ? 'subdir = $1' : (search ? '1=1' : 'subdir is null')} ${search ? `and filename ilike '%${search.replace(/'/g, "''")}%'` : ''}
|
|
63
|
-
${q ? ` and (${q})` : ''}`,
|
|
64
|
-
[subdir].filter(Boolean),
|
|
65
|
-
).then(el => el.rows || []); // ?.filter(row => items.map(el => el.name).includes(row.filename))
|
|
66
|
-
|
|
67
|
-
const subdirs = items
|
|
68
|
-
.filter(el => el.isDirectory())
|
|
69
|
-
.map(el => ({ type: 'dir', name: el.name }))
|
|
70
|
-
.filter(el => search ? el.name.includes(search) : true);
|
|
71
|
-
|
|
72
|
-
if (subdirs.length) {
|
|
73
|
-
await Promise.all(subdirs.map(async (item) => {
|
|
74
|
-
const items = isDirectory ? await readdir(path.join(dirpath, item.name)) : [];
|
|
75
|
-
Object.assign(item, { count: items.length });
|
|
76
|
-
}));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const result = { relpath };
|
|
80
|
-
|
|
81
|
-
if (config.debug) {
|
|
82
|
-
Object.assign(result, { rootDir });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// TODO: Фільтрувати по stats.mtime?
|
|
86
|
-
const allowedNames = Boolean(query?.filter) || Boolean(q) ? new Set(rows.map(r => r.filename)) : null;
|
|
87
|
-
const filteredFiles = (search ? allItems.filter(el => el.name.includes(search)) : items)
|
|
88
|
-
.filter(el => el.isFile())
|
|
89
|
-
.filter(el => !allowedNames || allowedNames.has(el.name));
|
|
90
|
-
|
|
91
|
-
const files = await Promise.all(filteredFiles.map(async el => {
|
|
92
|
-
const media = rows.find(row => row.filename === el.name);
|
|
93
|
-
const filepath = media ? media.url : ('/files/' + (el.path.split('/files/')[1] ? el.path.split('/files/')[1] + '/' : '') + el.name);
|
|
94
|
-
|
|
95
|
-
if (!filesizeCache[filepath]) {
|
|
96
|
-
const { mtime, size } = await stat(path.join(rootDir, filepath));
|
|
97
|
-
Object.assign(filesizeCache, { [filepath]: size });
|
|
98
|
-
mtime.setHours(0, 0, 0, 0); // strip hours from timestamp, leave only date
|
|
99
|
-
Object.assign(fileMtimeCache, { [filepath]: mtime });
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const [, from, to] = q ? (q.match(/between\s+'(\d{4}-\d{2}-\d{2})'::date\s+and\s+'(\d{4}-\d{2}-\d{2})'::date/i) || []) : [];
|
|
104
|
-
|
|
105
|
-
// skip entirely if invalid dates passed to query.filter
|
|
106
|
-
if ((from && new Date(from).toString() === 'Invalid Date') || (to && new Date(to).toString() === 'Invalid Date')) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// skip if file modification date does not match filter value
|
|
111
|
-
if (fileMtimeCache[filepath] && from && to && (new Date(from) > fileMtimeCache[filepath] || new Date(to) < fileMtimeCache[filepath])) {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const dt = q && !q.includes('between') ? query.filter.match(/\d{4}-\d{2}-\d{2}/)?.[0] : null;
|
|
116
|
-
const date = dt ? new Date(dt) : null;
|
|
117
|
-
// skip timezone offset
|
|
118
|
-
if (date) { date.setHours(0, 0, 0, 0); }
|
|
119
|
-
|
|
120
|
-
// filter by exact date
|
|
121
|
-
if (fileMtimeCache[filepath] && date && (new Date(date).getTime() !== fileMtimeCache[filepath].getTime())) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const mime = getMimeType(el.name) || '';
|
|
126
|
-
const preview = mime.startsWith('image/') /* && mime !== 'image/svg+xml' */
|
|
127
|
-
? `${req.routeOptions.url}/${media ? media.id : Buffer.from(filepath).toString('base64url')}/preview`
|
|
128
|
-
: undefined;
|
|
129
|
-
|
|
130
|
-
return media ? {
|
|
131
|
-
type: 'file',
|
|
132
|
-
...media,
|
|
133
|
-
url: `${req.routeOptions.url}/${media.id}/file`,
|
|
134
|
-
preview,
|
|
135
|
-
filepath,
|
|
136
|
-
metadata: `${req.routeOptions.url}/${media.id}`,
|
|
137
|
-
} : {
|
|
138
|
-
id: createHash('md5').update(filepath).digest('hex'),
|
|
139
|
-
hash: true,
|
|
140
|
-
type: 'file',
|
|
141
|
-
filename: el.name,
|
|
142
|
-
filepath,
|
|
143
|
-
filetype: mime.startsWith('image/') ? "image" : "other",
|
|
144
|
-
filesize: filesizeCache[filepath] || 0,
|
|
145
|
-
url: filepath,
|
|
146
|
-
mime,
|
|
147
|
-
preview,
|
|
148
|
-
updated_at: fileMtimeCache[filepath].toISOString(),
|
|
149
|
-
}
|
|
150
|
-
}));
|
|
151
|
-
|
|
152
|
-
// filter empty items, such as filtered out by query.filter=updated_at (not from db, but from fs.stat)
|
|
153
|
-
Object.assign(result, { data: subdirs.concat(files.filter(Boolean)) });
|
|
154
|
-
return result;
|
|
155
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
import { config, getFolder, pgClients, getMimeType, getFilterSQL } from '@opengis/fastify-table/utils.js';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
const filterList = [
|
|
9
|
+
{
|
|
10
|
+
name: "created_at",
|
|
11
|
+
type: "Date",
|
|
12
|
+
ua: "Дата створення"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "updated_at",
|
|
16
|
+
type: "Date",
|
|
17
|
+
ua: "Дата редагування"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
// path.resolve() converts POSIX paths from getFolder to valid Windows paths (Bun/Node fs require this on Windows)
|
|
22
|
+
const rootDir = path.resolve(getFolder(config, 'local'));
|
|
23
|
+
const dir = '/files';
|
|
24
|
+
|
|
25
|
+
const filesizeCache = {};
|
|
26
|
+
const fileMtimeCache = {};
|
|
27
|
+
|
|
28
|
+
mkdirSync(path.join(rootDir, dir), { recursive: true });
|
|
29
|
+
|
|
30
|
+
export default async function listMedia(req, reply) {
|
|
31
|
+
const { pg = pgClients.client, query = {} } = req;
|
|
32
|
+
const { subdir: subdir1 = '', search } = query;
|
|
33
|
+
|
|
34
|
+
if (!pg.pk?.['site.media']) {
|
|
35
|
+
return reply.status(404).send('table not found');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof subdir1 !== 'string' || subdir1.includes('..')) {
|
|
39
|
+
return reply.status(403).send('invalid params: subdir');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const subdir = subdir1.replace(/\/{2,}/g, '/');
|
|
43
|
+
const relpath = path.join(dir, subdir1).replace(/\\/g, '/');
|
|
44
|
+
const dirpath = path.join(rootDir, relpath).replace(/\\/g, '/');
|
|
45
|
+
|
|
46
|
+
if (!existsSync(dirpath)) {
|
|
47
|
+
return { data: [], relpath, msg: 'directory not exists' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const isDirectory = (await stat(dirpath)).isDirectory();
|
|
51
|
+
|
|
52
|
+
const allItems = isDirectory ? await readdir(dirpath, { withFileTypes: true, recursive: true }) : [];
|
|
53
|
+
const items = isDirectory ? await readdir(dirpath, { withFileTypes: true }) : [];
|
|
54
|
+
|
|
55
|
+
const { q = '' } = await getFilterSQL({ table: 'site.media', filter: query?.filter, filterList, pg });
|
|
56
|
+
|
|
57
|
+
const rows = await pg.query(
|
|
58
|
+
`select
|
|
59
|
+
media_id as id, filename, filetype, filesize, url, description, alt,
|
|
60
|
+
mime, preview_url, created_at, updated_at, created_by, updated_by
|
|
61
|
+
from site.media
|
|
62
|
+
where ${subdir ? 'subdir = $1' : (search ? '1=1' : 'subdir is null')} ${search ? `and filename ilike '%${search.replace(/'/g, "''")}%'` : ''}
|
|
63
|
+
${q ? ` and (${q})` : ''}`,
|
|
64
|
+
[subdir].filter(Boolean),
|
|
65
|
+
).then(el => el.rows || []); // ?.filter(row => items.map(el => el.name).includes(row.filename))
|
|
66
|
+
|
|
67
|
+
const subdirs = items
|
|
68
|
+
.filter(el => el.isDirectory())
|
|
69
|
+
.map(el => ({ type: 'dir', name: el.name }))
|
|
70
|
+
.filter(el => search ? el.name.includes(search) : true);
|
|
71
|
+
|
|
72
|
+
if (subdirs.length) {
|
|
73
|
+
await Promise.all(subdirs.map(async (item) => {
|
|
74
|
+
const items = isDirectory ? await readdir(path.join(dirpath, item.name)) : [];
|
|
75
|
+
Object.assign(item, { count: items.length });
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = { relpath };
|
|
80
|
+
|
|
81
|
+
if (config.debug) {
|
|
82
|
+
Object.assign(result, { rootDir });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// TODO: Фільтрувати по stats.mtime?
|
|
86
|
+
const allowedNames = Boolean(query?.filter) || Boolean(q) ? new Set(rows.map(r => r.filename)) : null;
|
|
87
|
+
const filteredFiles = (search ? allItems.filter(el => el.name.includes(search)) : items)
|
|
88
|
+
.filter(el => el.isFile())
|
|
89
|
+
.filter(el => !allowedNames || allowedNames.has(el.name));
|
|
90
|
+
|
|
91
|
+
const files = await Promise.all(filteredFiles.map(async el => {
|
|
92
|
+
const media = rows.find(row => row.filename === el.name);
|
|
93
|
+
const filepath = media ? media.url : ('/files/' + (el.path.split('/files/')[1] ? el.path.split('/files/')[1] + '/' : '') + el.name);
|
|
94
|
+
|
|
95
|
+
if (!filesizeCache[filepath]) {
|
|
96
|
+
const { mtime, size } = await stat(path.join(rootDir, filepath));
|
|
97
|
+
Object.assign(filesizeCache, { [filepath]: size });
|
|
98
|
+
mtime.setHours(0, 0, 0, 0); // strip hours from timestamp, leave only date
|
|
99
|
+
Object.assign(fileMtimeCache, { [filepath]: mtime });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
const [, from, to] = q ? (q.match(/between\s+'(\d{4}-\d{2}-\d{2})'::date\s+and\s+'(\d{4}-\d{2}-\d{2})'::date/i) || []) : [];
|
|
104
|
+
|
|
105
|
+
// skip entirely if invalid dates passed to query.filter
|
|
106
|
+
if ((from && new Date(from).toString() === 'Invalid Date') || (to && new Date(to).toString() === 'Invalid Date')) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// skip if file modification date does not match filter value
|
|
111
|
+
if (fileMtimeCache[filepath] && from && to && (new Date(from) > fileMtimeCache[filepath] || new Date(to) < fileMtimeCache[filepath])) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const dt = q && !q.includes('between') ? query.filter.match(/\d{4}-\d{2}-\d{2}/)?.[0] : null;
|
|
116
|
+
const date = dt ? new Date(dt) : null;
|
|
117
|
+
// skip timezone offset
|
|
118
|
+
if (date) { date.setHours(0, 0, 0, 0); }
|
|
119
|
+
|
|
120
|
+
// filter by exact date
|
|
121
|
+
if (fileMtimeCache[filepath] && date && (new Date(date).getTime() !== fileMtimeCache[filepath].getTime())) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const mime = getMimeType(el.name) || '';
|
|
126
|
+
const preview = mime.startsWith('image/') /* && mime !== 'image/svg+xml' */
|
|
127
|
+
? `${req.routeOptions.url}/${media ? media.id : Buffer.from(filepath).toString('base64url')}/preview`
|
|
128
|
+
: undefined;
|
|
129
|
+
|
|
130
|
+
return media ? {
|
|
131
|
+
type: 'file',
|
|
132
|
+
...media,
|
|
133
|
+
url: `${req.routeOptions.url}/${media.id}/file`,
|
|
134
|
+
preview,
|
|
135
|
+
filepath,
|
|
136
|
+
metadata: `${req.routeOptions.url}/${media.id}`,
|
|
137
|
+
} : {
|
|
138
|
+
id: createHash('md5').update(filepath).digest('hex'),
|
|
139
|
+
hash: true,
|
|
140
|
+
type: 'file',
|
|
141
|
+
filename: el.name,
|
|
142
|
+
filepath,
|
|
143
|
+
filetype: mime.startsWith('image/') ? "image" : "other",
|
|
144
|
+
filesize: filesizeCache[filepath] || 0,
|
|
145
|
+
url: filepath,
|
|
146
|
+
mime,
|
|
147
|
+
preview,
|
|
148
|
+
updated_at: fileMtimeCache[filepath].toISOString(),
|
|
149
|
+
}
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
// filter empty items, such as filtered out by query.filter=updated_at (not from db, but from fs.stat)
|
|
153
|
+
Object.assign(result, { data: subdirs.concat(files.filter(Boolean)) });
|
|
154
|
+
return result;
|
|
155
|
+
|
|
156
156
|
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
|
|
4
|
-
import { config, getFolder, pgClients } from "@opengis/fastify-table/utils.js";
|
|
5
|
-
|
|
6
|
-
// path.resolve() converts POSIX paths from getFolder to valid Windows paths (Bun/Node fs require this on Windows)
|
|
7
|
-
const rootDir = path.resolve(getFolder(config, 'local'));
|
|
8
|
-
|
|
9
|
-
export default async function metadataMedia({
|
|
10
|
-
routeOptions = {}, pg = pgClients.client, params = {},
|
|
11
|
-
}, reply) {
|
|
12
|
-
if (!params?.id) {
|
|
13
|
-
return reply.status(400).send('not enough params: id');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (!pg.pk?.['site.media']) {
|
|
17
|
-
return reply.status(404).send('table not found');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const data = await pg.query(
|
|
21
|
-
'select * from site.media where media_id = $1 and url is not null',
|
|
22
|
-
[params.id],
|
|
23
|
-
).then(el => el.rows?.[0]);
|
|
24
|
-
|
|
25
|
-
if (!data) {
|
|
26
|
-
return reply.status(404).send('media not found: ' + params.id);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const filepath = path.join(rootDir, data.url);
|
|
30
|
-
|
|
31
|
-
Object.assign(data, {
|
|
32
|
-
url: `${routeOptions.url.replace(':id', params.id)}/file`,
|
|
33
|
-
preview: `${routeOptions.url.replace(':id', params.id)}/preview`,
|
|
34
|
-
filepath: data.url,
|
|
35
|
-
exists: existsSync(filepath),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
return reply.status(200).send(data);
|
|
39
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import { config, getFolder, pgClients } from "@opengis/fastify-table/utils.js";
|
|
5
|
+
|
|
6
|
+
// path.resolve() converts POSIX paths from getFolder to valid Windows paths (Bun/Node fs require this on Windows)
|
|
7
|
+
const rootDir = path.resolve(getFolder(config, 'local'));
|
|
8
|
+
|
|
9
|
+
export default async function metadataMedia({
|
|
10
|
+
routeOptions = {}, pg = pgClients.client, params = {},
|
|
11
|
+
}, reply) {
|
|
12
|
+
if (!params?.id) {
|
|
13
|
+
return reply.status(400).send('not enough params: id');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!pg.pk?.['site.media']) {
|
|
17
|
+
return reply.status(404).send('table not found');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const data = await pg.query(
|
|
21
|
+
'select * from site.media where media_id = $1 and url is not null',
|
|
22
|
+
[params.id],
|
|
23
|
+
).then(el => el.rows?.[0]);
|
|
24
|
+
|
|
25
|
+
if (!data) {
|
|
26
|
+
return reply.status(404).send('media not found: ' + params.id);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const filepath = path.join(rootDir, data.url);
|
|
30
|
+
|
|
31
|
+
Object.assign(data, {
|
|
32
|
+
url: `${routeOptions.url.replace(':id', params.id)}/file`,
|
|
33
|
+
preview: `${routeOptions.url.replace(':id', params.id)}/preview`,
|
|
34
|
+
filepath: data.url,
|
|
35
|
+
exists: existsSync(filepath),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return reply.status(200).send(data);
|
|
39
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import getSettings from '../functions/getSettings.js';
|
|
2
|
-
|
|
3
|
-
export default async function getAppSettings({ query, params, user }, reply) {
|
|
4
|
-
const t1 = Date.now();
|
|
5
|
-
|
|
6
|
-
const result = await getSettings({
|
|
7
|
-
entity: params.entity,
|
|
8
|
-
keys: query.keys,
|
|
9
|
-
user,
|
|
10
|
-
ttl: 0
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
if (result.error) {
|
|
14
|
-
return reply.status(result.code).send(result);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return { time: Date.now() - t1, uid: user?.uid, settings: result };
|
|
18
|
-
}
|
|
1
|
+
import getSettings from '../functions/getSettings.js';
|
|
2
|
+
|
|
3
|
+
export default async function getAppSettings({ query, params, user }, reply) {
|
|
4
|
+
const t1 = Date.now();
|
|
5
|
+
|
|
6
|
+
const result = await getSettings({
|
|
7
|
+
entity: params.entity,
|
|
8
|
+
keys: query.keys,
|
|
9
|
+
user,
|
|
10
|
+
ttl: 0
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (result.error) {
|
|
14
|
+
return reply.status(result.code).send(result);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { time: Date.now() - t1, uid: user?.uid, settings: result };
|
|
18
|
+
}
|
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
import { config, logger, dataInsert, pgClients } from '@opengis/fastify-table/utils.js';
|
|
2
|
-
|
|
3
|
-
function checkValueType(val) {
|
|
4
|
-
if (val && typeof val === "object") {
|
|
5
|
-
return "property_json";
|
|
6
|
-
}
|
|
7
|
-
if (
|
|
8
|
-
val &&
|
|
9
|
-
typeof val === "number" &&
|
|
10
|
-
!/\D/.test(val.toString()) &&
|
|
11
|
-
val.toString().length < 10
|
|
12
|
-
) {
|
|
13
|
-
return "property_int";
|
|
14
|
-
}
|
|
15
|
-
return "property_text";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default async function postAppSettings(
|
|
19
|
-
{
|
|
20
|
-
pg = pgClients.client,
|
|
21
|
-
body,
|
|
22
|
-
user,
|
|
23
|
-
params,
|
|
24
|
-
},
|
|
25
|
-
reply
|
|
26
|
-
) {
|
|
27
|
-
const { uid } = user || {};
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
(!params?.entity || params.entity === "app") &&
|
|
31
|
-
!user?.user_type?.includes?.("admin") &&
|
|
32
|
-
!config.local
|
|
33
|
-
) {
|
|
34
|
-
return reply.status(403).send("access restricted");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!pg) {
|
|
38
|
-
return reply.status(500).send("empty pg");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (!pg?.pk?.["admin.properties"]) {
|
|
42
|
-
return reply.status(404).send("table not found");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const { key, val } = body;
|
|
46
|
-
|
|
47
|
-
if ((!key || !val) && !Object.keys(body).length) {
|
|
48
|
-
return reply.status(400).send("not enough body params");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const keys = Object.keys(body);
|
|
52
|
-
|
|
53
|
-
const entity = params?.entity === "user" ? user.uid : params?.entity || "app";
|
|
54
|
-
|
|
55
|
-
const client = await pg.connect();
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
await client.query("begin;");
|
|
59
|
-
|
|
60
|
-
await client.query(
|
|
61
|
-
"delete from admin.properties where property_entity=$1 and property_key=any($2)",
|
|
62
|
-
[entity, keys]
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
await Promise.all(
|
|
66
|
-
keys
|
|
67
|
-
.filter((el) => body[el])
|
|
68
|
-
.map(async (el) => {
|
|
69
|
-
const columnType = checkValueType(body[el]);
|
|
70
|
-
|
|
71
|
-
await dataInsert({
|
|
72
|
-
pg: client,
|
|
73
|
-
table: "admin.properties",
|
|
74
|
-
data: {
|
|
75
|
-
property_key: el,
|
|
76
|
-
[columnType]: body[el],
|
|
77
|
-
property_entity: entity,
|
|
78
|
-
},
|
|
79
|
-
uid,
|
|
80
|
-
});
|
|
81
|
-
})
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
await client.query("commit;");
|
|
85
|
-
|
|
86
|
-
return reply.status(200).send("ok");
|
|
87
|
-
} catch (err) {
|
|
88
|
-
logger.file("properties/error", {
|
|
89
|
-
error: err.toString(),
|
|
90
|
-
stack: err.stack,
|
|
91
|
-
body,
|
|
92
|
-
user,
|
|
93
|
-
entity,
|
|
94
|
-
});
|
|
95
|
-
return reply.status(500).send(err.toString());
|
|
96
|
-
} finally {
|
|
97
|
-
client.release();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
1
|
+
import { config, logger, dataInsert, pgClients } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
3
|
+
function checkValueType(val) {
|
|
4
|
+
if (val && typeof val === "object") {
|
|
5
|
+
return "property_json";
|
|
6
|
+
}
|
|
7
|
+
if (
|
|
8
|
+
val &&
|
|
9
|
+
typeof val === "number" &&
|
|
10
|
+
!/\D/.test(val.toString()) &&
|
|
11
|
+
val.toString().length < 10
|
|
12
|
+
) {
|
|
13
|
+
return "property_int";
|
|
14
|
+
}
|
|
15
|
+
return "property_text";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default async function postAppSettings(
|
|
19
|
+
{
|
|
20
|
+
pg = pgClients.client,
|
|
21
|
+
body,
|
|
22
|
+
user,
|
|
23
|
+
params,
|
|
24
|
+
},
|
|
25
|
+
reply
|
|
26
|
+
) {
|
|
27
|
+
const { uid } = user || {};
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
(!params?.entity || params.entity === "app") &&
|
|
31
|
+
!user?.user_type?.includes?.("admin") &&
|
|
32
|
+
!config.local
|
|
33
|
+
) {
|
|
34
|
+
return reply.status(403).send("access restricted");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!pg) {
|
|
38
|
+
return reply.status(500).send("empty pg");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!pg?.pk?.["admin.properties"]) {
|
|
42
|
+
return reply.status(404).send("table not found");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { key, val } = body;
|
|
46
|
+
|
|
47
|
+
if ((!key || !val) && !Object.keys(body).length) {
|
|
48
|
+
return reply.status(400).send("not enough body params");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const keys = Object.keys(body);
|
|
52
|
+
|
|
53
|
+
const entity = params?.entity === "user" ? user.uid : params?.entity || "app";
|
|
54
|
+
|
|
55
|
+
const client = await pg.connect();
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await client.query("begin;");
|
|
59
|
+
|
|
60
|
+
await client.query(
|
|
61
|
+
"delete from admin.properties where property_entity=$1 and property_key=any($2)",
|
|
62
|
+
[entity, keys]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
await Promise.all(
|
|
66
|
+
keys
|
|
67
|
+
.filter((el) => body[el])
|
|
68
|
+
.map(async (el) => {
|
|
69
|
+
const columnType = checkValueType(body[el]);
|
|
70
|
+
|
|
71
|
+
await dataInsert({
|
|
72
|
+
pg: client,
|
|
73
|
+
table: "admin.properties",
|
|
74
|
+
data: {
|
|
75
|
+
property_key: el,
|
|
76
|
+
[columnType]: body[el],
|
|
77
|
+
property_entity: entity,
|
|
78
|
+
},
|
|
79
|
+
uid,
|
|
80
|
+
});
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
await client.query("commit;");
|
|
85
|
+
|
|
86
|
+
return reply.status(200).send("ok");
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.file("properties/error", {
|
|
89
|
+
error: err.toString(),
|
|
90
|
+
stack: err.stack,
|
|
91
|
+
body,
|
|
92
|
+
user,
|
|
93
|
+
entity,
|
|
94
|
+
});
|
|
95
|
+
return reply.status(500).send(err.toString());
|
|
96
|
+
} finally {
|
|
97
|
+
client.release();
|
|
98
|
+
}
|
|
99
|
+
}
|