@opengis/cms 0.0.16 → 0.0.18

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 (201) hide show
  1. package/README.md +96 -3
  2. package/dist/assets/AddUser-CX-McfRW.js +1 -0
  3. package/dist/assets/ApiKeys-DSv1exYv.js +16 -0
  4. package/dist/assets/Appearance-DDtOUvCV.js +6 -0
  5. package/dist/assets/ArticlesPage-D6B3cZsl.js +6 -0
  6. package/dist/assets/BuilderPage-CCeSMVWe.js +1 -0
  7. package/dist/assets/CollectionsBreadcrumb.vue_vue_type_script_setup_true_lang-DVYVfYF4.js +1 -0
  8. package/dist/assets/CollectionsPage-CHk8Cn5k.js +1 -0
  9. package/dist/assets/Dashboard-Bs7sXO6h.js +11 -0
  10. package/dist/assets/EditCollectionPage-gPuLJrN8.js +41 -0
  11. package/dist/assets/EmailPage-DhlWsPxk.js +1 -0
  12. package/dist/assets/EmptyData-Ct-xQv_N.js +1 -0
  13. package/dist/assets/FeedbackPage-DtaOncVv.js +1 -0
  14. package/dist/assets/Logs-CZ5klHNK.js +1 -0
  15. package/dist/assets/MediaBreadcrumb-BpOxt5PK.js +11 -0
  16. package/dist/assets/MediaPage-DqRcZFlO.js +16 -0
  17. package/dist/assets/MenuAddPage-BLcoVgrS.js +1 -0
  18. package/dist/assets/MenuItemPage-B2otXqkz.js +20 -0
  19. package/dist/assets/MenuPage-C00m4Fc_.js +1 -0
  20. package/dist/assets/MonacoEditor.vue_vue_type_script_setup_true_lang-CjoEsC67.js +3 -0
  21. package/dist/assets/PermissionsPage-LtqcCJ14.js +1 -0
  22. package/dist/assets/Settings-BHH6RoBP.js +1 -0
  23. package/dist/assets/SettingsTable-CY5pZx1z.js +1 -0
  24. package/dist/assets/SettingsTitle-t4WJBFxZ.js +1 -0
  25. package/dist/assets/SingletonsPage-Bn2Ypjhs.js +6 -0
  26. package/dist/assets/TagsPage-DAiakEth.js +1 -0
  27. package/dist/assets/UniversalTable.vue_vue_type_script_setup_true_lang-BQ5m4aZd.js +11 -0
  28. package/dist/assets/UniversalTablePagination.vue_vue_type_script_setup_true_lang-DppVBws0.js +1 -0
  29. package/dist/assets/Users-CMH5j0db.js +1 -0
  30. package/dist/assets/UsersPage-CDGreEib.js +1 -0
  31. package/dist/assets/arrow-up-DCe0WsrM.js +16 -0
  32. package/dist/assets/{calendar-hsWc4yH-.js → calendar-o9t4MkD2.js} +1 -1
  33. package/dist/assets/chevron-left-WFftVS9c.js +6 -0
  34. package/dist/assets/chevron-right-BiiSb3Be.js +6 -0
  35. package/dist/assets/contentForm-unZQhjCu.js +6 -0
  36. package/dist/assets/en-BDx3Svx8.js +1 -0
  37. package/dist/assets/eye-Dijywc6g.js +6 -0
  38. package/dist/assets/file-B_duymIT.js +6 -0
  39. package/dist/assets/general-CkN_0qIV.js +1 -0
  40. package/dist/assets/index-BIp7eSXk.js +1 -0
  41. package/dist/assets/index-DCW2e4Az.js +9 -0
  42. package/dist/assets/index-DGweaj24.js +1 -0
  43. package/dist/assets/index-W-qQIppj-BDlsxaGB.js +1 -0
  44. package/dist/assets/index-W-qQIppj-BsopI3Hz-BIZR-dhy.js +1 -0
  45. package/dist/assets/index-oQz9FOqL.css +1 -0
  46. package/dist/assets/index-yMJAVBXk.js +290 -0
  47. package/dist/assets/list-CXRbSNky.js +6 -0
  48. package/dist/assets/pencil-CwnPP4IJ.js +6 -0
  49. package/dist/assets/{plus-D9etvrM2.js → plus-DLR44m6p.js} +1 -1
  50. package/dist/assets/save-FeDrOUOd.js +6 -0
  51. package/dist/assets/{search-BI-hqhq6.js → search-C4-fHihx.js} +1 -1
  52. package/dist/assets/{square-pen-61CkyXzK.js → square-pen-xVs4e8Yb.js} +1 -1
  53. package/dist/assets/{trash-2-CJSl_r88.js → trash-2-BGXMNU3d.js} +1 -1
  54. package/dist/assets/uk-BA7DIKEL.js +1 -0
  55. package/dist/assets/useDebounce-DFq3rxAW.js +1 -0
  56. package/dist/assets/vs-form-reletion-link-C-xrdHDl.js +20 -0
  57. package/dist/assets/vs-form-reletion-link-bk-9ZkDH.css +1 -0
  58. package/dist/assets/vue.-sixQ7xP-CUPNuJcq.js +1 -0
  59. package/dist/assets/vuedraggable.umd-W_2WTF6i.js +14 -0
  60. package/dist/assets/{x-BNquQe5y.js → x-D2t-wfBe.js} +1 -1
  61. package/dist/index.html +14 -9
  62. package/module/cms/card/cms.content.table/index.yml +17 -0
  63. package/module/cms/card/cms.content.table/main_info.hbs +26 -0
  64. package/module/cms/card/cms.menu.table/content_info.hbs +16 -0
  65. package/module/cms/card/cms.menu.table/index.yml +18 -0
  66. package/module/cms/card/cms.menu.table/main_info.hbs +22 -0
  67. package/module/cms/card/cms.settings.table/index.yml +13 -0
  68. package/module/cms/card/cms.settings.table/main_info.hbs +20 -0
  69. package/module/cms/cls/content.status.json +18 -0
  70. package/module/cms/cls/user_type.json +10 -0
  71. package/module/cms/form/admin.users.form.json +78 -0
  72. package/module/cms/form/cms.content.form.json +79 -0
  73. package/module/cms/form/cms.menu.form.json +69 -0
  74. package/module/cms/form/cms.settings.form.json +32 -0
  75. package/module/cms/menu.json +24 -0
  76. package/module/cms/router.js +154 -0
  77. package/module/cms/select/cms.page_type.sql +2 -0
  78. package/module/cms/select/collection.sql +1 -0
  79. package/module/cms/select/locale.sql +17 -0
  80. package/module/cms/select/news_tag_id.sql +12 -0
  81. package/module/cms/select/tag_id.sql +1 -0
  82. package/module/cms/table/admin.users.table.json +54 -0
  83. package/module/cms/table/cms.content.table.json +106 -0
  84. package/module/cms/table/cms.menu.table.json +73 -0
  85. package/module/cms/table/cms.settings.table.json +57 -0
  86. package/module/cms/table/collection.default.table.json +102 -0
  87. package/module/cms/table/single.default.table.json +115 -0
  88. package/package.json +36 -31
  89. package/plugin.js +63 -23
  90. package/server/app.js +20 -3
  91. package/server/functions/getDraftKey.js +22 -0
  92. package/server/index.js +2 -3
  93. package/server/migrations/fixes.sql +124 -0
  94. package/server/migrations/site.sql +338 -249
  95. package/server/plugins/adminHook.js +2 -2
  96. package/server/plugins/hook.js +53 -61
  97. package/server/plugins/vite.js +5 -5
  98. package/server/routes/cms/controllers/cmsStat.js +56 -0
  99. package/server/routes/cms/controllers/cmsSuggest.js +58 -0
  100. package/server/routes/cms/controllers/deleteContent.js +114 -59
  101. package/server/routes/cms/controllers/deleteMedia.js +75 -46
  102. package/server/routes/cms/controllers/downloadMedia.js +48 -48
  103. package/server/routes/cms/controllers/getContent.js +110 -95
  104. package/server/routes/cms/controllers/getContentBySlug.js +95 -0
  105. package/server/routes/cms/controllers/getPermissions.js +15 -15
  106. package/server/routes/cms/controllers/insertContent.js +218 -68
  107. package/server/routes/cms/controllers/listMedia.js +93 -72
  108. package/server/routes/cms/controllers/metadataMedia.js +38 -37
  109. package/server/routes/cms/controllers/properties.get.js +53 -0
  110. package/server/routes/cms/controllers/properties.post.js +99 -0
  111. package/server/routes/cms/controllers/searchContent.js +205 -0
  112. package/server/routes/cms/controllers/setPermissions.js +49 -49
  113. package/server/routes/cms/controllers/translate.js +90 -0
  114. package/server/routes/cms/controllers/updateContent.js +238 -111
  115. package/server/routes/cms/controllers/uploadMedia.js +78 -65
  116. package/server/routes/cms/index.mjs +81 -12
  117. package/server/routes/cms/utils/additionalData.js +36 -0
  118. package/server/routes/cms/utils/getCollection.js +82 -0
  119. package/server/routes/cms/utils/getSingle.js +188 -0
  120. package/server/routes/cms/utils/insertContentLocalization.js +87 -0
  121. package/server/routes/cms/utils/requestTranslation.js +85 -0
  122. package/server/routes/cms/utils/updateLocalization.js +48 -0
  123. package/server/routes/cmsSpace/controllers/deleteSpace.js +26 -0
  124. package/server/routes/cmsSpace/controllers/getSpaces.js +28 -0
  125. package/server/routes/cmsSpace/controllers/insertSpace.js +22 -0
  126. package/server/routes/cmsSpace/controllers/updateSpace.js +24 -0
  127. package/server/routes/cmsSpace/index.mjs +20 -0
  128. package/server/routes/contentType/controllers/addContentType.js +162 -0
  129. package/server/routes/contentType/controllers/contentTypeList.js +54 -0
  130. package/server/routes/contentType/controllers/delContentType.js +75 -0
  131. package/server/routes/contentType/controllers/editContentType.js +61 -0
  132. package/server/routes/contentType/controllers/getContentType.js +37 -0
  133. package/server/routes/contentType/index.mjs +29 -19
  134. package/server/routes/contentType/utils/updateContents.js +29 -0
  135. package/server/routes/contentType/utils/updateCustomContentTable.js +56 -0
  136. package/server/routes/feedback/controllers/email.list.js +25 -0
  137. package/server/routes/feedback/controllers/feedback.js +49 -0
  138. package/server/routes/feedback/controllers/feedback.list.js +38 -0
  139. package/server/routes/feedback/controllers/news.subscriptions.js +44 -0
  140. package/server/routes/feedback/index.mjs +72 -0
  141. package/server/routes/logs/controllers/export.user.logs.js +78 -0
  142. package/server/routes/logs/controllers/user.logs.js +45 -0
  143. package/server/routes/logs/index.mjs +9 -0
  144. package/server/routes/menu/controllers/addMenu.js +38 -0
  145. package/server/routes/menu/controllers/delMenu.js +32 -0
  146. package/server/routes/menu/controllers/editMenu.js +42 -0
  147. package/server/routes/menu/controllers/getMenu.js +43 -0
  148. package/server/routes/menu/index.mjs +13 -0
  149. package/server/routes/migration/controllers/collectionToCustom.js +137 -0
  150. package/server/routes/migration/index.mjs +8 -0
  151. package/server/routes/tags/controllers/add.tags.js +25 -0
  152. package/server/routes/tags/controllers/del.tags.js +20 -0
  153. package/server/routes/tags/controllers/edit.tags.js +26 -0
  154. package/server/routes/tags/controllers/get.tags.js +16 -0
  155. package/server/routes/tags/index.mjs +14 -0
  156. package/server/templates/page/login.html +73 -5
  157. package/server/templates/select/core.user_mentioned.sql +2 -0
  158. package/src/index.ts +122 -0
  159. package/dist/assets/ArticlesPage-BveM4q3g.js +0 -11
  160. package/dist/assets/CollectionsPage-D5td-UBm.js +0 -1
  161. package/dist/assets/ContentBlock.vue_vue_type_script_setup_true_lang-BwF6D-yB.js +0 -30
  162. package/dist/assets/CreateCollectionPage-Cu0RW5ui.js +0 -76
  163. package/dist/assets/Dashboard-faSjwmB8.js +0 -11
  164. package/dist/assets/EditCollectionPage-K5oPPzCd.js +0 -1
  165. package/dist/assets/MediaPage-BoW3aWgN.js +0 -1
  166. package/dist/assets/PermissionsPage-DGy5fha2.js +0 -1
  167. package/dist/assets/SingletonsPage-C1X2xkQE.js +0 -1
  168. package/dist/assets/UniversalTable.vue_vue_type_script_setup_true_lang-DUqfWJcy.js +0 -6
  169. package/dist/assets/contentForm-DMVC4vho.js +0 -1
  170. package/dist/assets/database-BTxZQzYy.js +0 -6
  171. package/dist/assets/index-9GY17iSP.css +0 -1
  172. package/dist/assets/index-DYyZmLWO.js +0 -2138
  173. package/dist/assets/index-xsH4HHeE.js +0 -6
  174. package/dist/assets/save-C2B6th9J.js +0 -11
  175. package/dist/assets/settings-DbyDiH2g.js +0 -6
  176. package/dist/assets/vue.-sixQ7xP-DwXf3zRn.js +0 -1
  177. package/dist/assets/x-circle-C3q70RMH.js +0 -16
  178. package/server/routes/contentType/controllers/cms.type.delete.js +0 -22
  179. package/server/routes/contentType/controllers/cms.type.get.js +0 -22
  180. package/server/routes/contentType/controllers/cms.type.list.js +0 -25
  181. package/server/routes/contentType/controllers/cms.type.post.js +0 -22
  182. package/server/routes/contentType/controllers/cms.type.put.js +0 -24
  183. package/server/routes/contentType/utils/builderCache.js +0 -58
  184. package/server/routes/fileContent/data/deleteContent.js +0 -34
  185. package/server/routes/fileContent/data/deleteMedia.js +0 -28
  186. package/server/routes/fileContent/data/downloadMedia.js +0 -41
  187. package/server/routes/fileContent/data/getContent.js +0 -32
  188. package/server/routes/fileContent/data/insertContent.js +0 -37
  189. package/server/routes/fileContent/data/listMedia.js +0 -47
  190. package/server/routes/fileContent/data/metadataMedia.js +0 -38
  191. package/server/routes/fileContent/data/updateContent.js +0 -40
  192. package/server/routes/fileContent/data/uploadMedia.js +0 -49
  193. package/server/routes/fileContent/index.mjs +0 -54
  194. package/server/routes/fileContent/type/contentTypeList.js +0 -7
  195. package/server/routes/fileContent/type/createContentType.js +0 -31
  196. package/server/routes/fileContent/type/deleteContentType.js +0 -29
  197. package/server/routes/fileContent/type/getContentType.js +0 -15
  198. package/server/routes/fileContent/type/updateContentType.js +0 -40
  199. package/server/routes/fileContent/utils/astroBuilderCache.js +0 -47
  200. package/server/routes/fileContent/utils/contentDir.js +0 -12
  201. package/server/routes/fileContent/utils/contentTypeExists.js +0 -15
@@ -1,112 +1,239 @@
1
- import { config, pgClients, dataUpdate, dataInsert } from '@opengis/fastify-table/utils.js';
2
-
3
- import builderCache from '../../contentType/utils/builderCache.js';
4
-
5
- export default async function updateContent(req, reply) {
6
- const {
7
- pg = pgClients.client,
8
- params = {},
9
- user = {},
10
- body = {},
11
- headers = {},
12
- } = req;
13
-
14
- const { type, id } = params;
15
-
16
- if (!type) {
17
- return reply.status(400).send('not enough params: type');
18
- }
19
-
20
- if (!id) {
21
- return reply.status(400).send('not enough params: id');
22
- }
23
-
24
- if (!Object.keys(body || {}).length) {
25
- return reply.status(400).send('empty body');
26
- }
27
-
28
- if (type === 'single') {
29
- const cache = builderCache(id) || {};
30
- if (!cache) {
31
- return reply.status(400).send('invalid params: id');
32
- }
33
-
34
- const client = await pg.connect();
35
-
36
- try {
37
- await client.query('BEGIN');
38
- // await client.query('SET statement_timeout = 100000000;');
39
- await client.query(`delete from site.single_type_values where type_name=$1`, [id]);
40
-
41
- await Promise.all(Object.keys(body).filter(key => body[key]).map(async key => dataInsert({
42
- pg: client,
43
- table: 'site.single_type_values',
44
- data: { key, value: body[key], type_name: id },
45
- referer: headers?.referer,
46
- uid: user?.uid || 0,
47
- })));
48
-
49
- // await client.query('SET statement_timeout = 0;');
50
-
51
- await client.query('COMMIT');
52
- return reply.status(200).send({ id, ...body });
53
- }
54
- catch (err) {
55
- await client.query('ROLLBACK');
56
- return reply.status(500).send(err.toString());
57
- }
58
- finally {
59
- client.release();
60
- }
61
- }
62
-
63
- if (builderCache[type]?.table && builderCache[type]?.provider === 'pg' && config.pg) {
64
- const result = await dataUpdate({
65
- pg,
66
- id,
67
- table: 'site.' + builderCache[type]?.table,
68
- data: body,
69
- referer: headers?.referer,
70
- uid: user?.uid || 0,
71
- }).catch(err => {
72
- if (err.message?.includes?.('unique constraint')) {
73
- return reply.status(400).send('Порушенні унікальності: ' + err.message?.match?.(/([^"]+)/g)?.[1]);
74
- }
75
- return reply.status(500).send(err.toString());
76
- });
77
-
78
- const pk = pg.pk?.['site.' + builderCache[type]?.table];
79
- return reply.status(200).send({ id: result?.[pk], ...result || {} });
80
- }
81
-
82
- const arr = config.pg ? await pg.query(`select array_agg(relname)::text[] from pg_class a
83
- left join pg_namespace b on a.relnamespace=b.oid
84
- where a.relam=2 and b.nspname='site'`).then(el => el.rows?.[0]?.array_agg || []) : [];
85
-
86
- if (!arr.length) {
87
- return reply.status(400).send('empty schema: site');
88
- }
89
-
90
- const table = arr.find(el => el === params.type);
91
-
92
- if (!table) {
93
- return reply.status(400).send('invalid params: type');
94
- }
95
-
96
- const result = await dataUpdate({
97
- pg,
98
- id,
99
- table: 'site.' + table,
100
- data: body,
101
- referer: headers?.referer,
102
- uid: user?.uid || 0,
103
- }).catch(err => {
104
- if (err.message?.includes?.('unique constraint')) {
105
- return reply.status(400).send('Порушенні унікальності: ' + err.message?.match?.(/([^"]+)/g)?.[1]);
106
- }
107
- return reply.status(500).send(err.toString());
108
- });
109
-
110
- const pk = pg.pk?.['site.' + table];
111
- return reply.status(200).send({ id: result?.[pk], ...result || {} });
1
+ import { config, pgClients, dataUpdate, dataInsert, getTemplate, checkSQL, logger } from '@opengis/fastify-table/utils.js';
2
+
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+
5
+ const inputTypes = existsSync('input-types.json') ? JSON.parse(readFileSync('input-types.json') || '{}') : {};
6
+
7
+ import updateLocalization from '../utils/updateLocalization.js';
8
+
9
+ const defaultColumns = [
10
+ 'content_id',
11
+ 'space_id',
12
+ 'content_type_id',
13
+ 'created_at',
14
+ 'updated_at',
15
+ 'published_at',
16
+ 'revision',
17
+ 'locale',
18
+ 'status',
19
+ 'slug',
20
+ 'title',
21
+ 'created_by',
22
+ 'published_by',
23
+ 'updated_by',
24
+ 'meta',
25
+ ];
26
+
27
+ export default async function updateContent(req, reply) {
28
+ const {
29
+ pg = pgClients.client,
30
+ params = {},
31
+ user = {},
32
+ body = {},
33
+ headers = {},
34
+ } = req;
35
+
36
+ const { type, id } = params;
37
+
38
+ if (!type) {
39
+ return reply.status(400).send('not enough params: type');
40
+ }
41
+
42
+ if (!id) {
43
+ return reply.status(400).send('not enough params: id');
44
+ }
45
+
46
+ if (!Object.keys(body || {}).length) {
47
+ return reply.status(400).send('empty body');
48
+ }
49
+
50
+ const arr = config.pg ? await pg.query(`select array_agg(relname)::text[] from pg_class a
51
+ left join pg_namespace b on a.relnamespace=b.oid
52
+ where a.relam=2 and b.nspname='data'`).then(el => el.rows?.[0]?.array_agg || []) : [];
53
+
54
+ if (!arr.length) {
55
+ return reply.status(400).send('empty schema: data');
56
+ }
57
+
58
+ const table = arr.find(el => el === params.type);
59
+
60
+ // order priority - custom columns -> default for pages
61
+ const { ctid, ctname, dbtable, columns: contentColumns = [] } = await pg.query('select content_type_id as ctid, name as ctname, table_name as dbtable, columns from site.content_types where content_type_id in (select content_type_id from site.contents where content_id=$1) or content_type_id=$2 order by content_type_id = \'pages\'', [id, type]).then(el => el.rows?.[0] || {});
62
+
63
+
64
+ const loadTable = type === 'pages' ? await getTemplate('table', 'single.default.table') : {};
65
+ const columns = type === 'pages'
66
+ ? (loadTable?.columns || []).concat(contentColumns.filter(col => loadTable?.columns.findIndex(el => el.name === col.name) === -1))
67
+ : contentColumns;
68
+
69
+ // const xss = checkXSS({ body });
70
+ const sql = checkSQL({ body });
71
+
72
+ if (/*xss.error ||*/ sql.error && false) {
73
+ logger.file(`injection/${/*xss.error ? 'xss' : */'sql'}/cms`, {
74
+ table,
75
+ ...params,
76
+ uid: user?.uid,
77
+ ...(/*xss.error ? xss : */sql),
78
+ });
79
+ return reply
80
+ .status(409)
81
+ .send(
82
+ `Дані містять заборонені ${/*xss.error ? 'xss' : */'sql'} символи. Приберіть їх та спробуйте ще раз`
83
+ );
84
+ }
85
+
86
+ // site.content_data, includes singletone
87
+ if (((!table && !dbtable) || ['single', 'pages'].includes(type)) && ctid) {
88
+ const cid = await pg.query(
89
+ 'select content_id from site.contents where content_type_id=$1 limit 1',
90
+ [ctid],
91
+ ).then(el => el.rows?.[0]?.content_id);
92
+
93
+ const ctid1 = body.content_type_id || ctid;
94
+
95
+ if (!cid) {
96
+ return reply.status(404).send('contents not found');
97
+ }
98
+
99
+ const contentId = cid === 'pages'
100
+ ? await pg.query('select content_id from site.content_data where object_id=$1', [id]).then(el => el.rows?.[0]?.content_id)
101
+ : cid;
102
+
103
+ if (!contentId) {
104
+ return reply.status(404).send('contents not found: 2');
105
+ }
106
+
107
+ const columnList = columns?.map?.(el => el.name) || [];
108
+ const types = columns?.reduce?.((acc, curr) => ({ ...acc, [curr.name]: curr.type || 'text' }), {}) || {};
109
+ const keys = Object.keys(body || {}).filter(key => columnList.includes(key) && !defaultColumns.includes(key));
110
+
111
+
112
+ if (!Object.keys(body || {}).length) {
113
+ return reply.status(400).send('invalid payload');
114
+ }
115
+
116
+ const blocks = await pg.query(`select json_object_agg(field_key,field_value) from site.content_data where content_id=$1 and field_type='reference'`, [id])
117
+ .then(el => el.rows?.[0]?.json_object_agg || {});
118
+
119
+ const emptyBlock = Object.keys(body).find(key => blocks[key] && (!body[key] || typeof body[key] !== 'object' || Object.keys(body[key] || {}) === 0));
120
+
121
+ if (emptyBlock) {
122
+ return reply.status(400).send('access restricted: empty/invalid block ' + emptyBlock);
123
+ }
124
+
125
+ const client = await pg.connect();
126
+
127
+ try {
128
+ await client.query('begin');
129
+ const res = await dataUpdate({
130
+ pg: client,
131
+ table: 'site.contents',
132
+ id,
133
+ data: { ...body, content_type_id: ctid1 },
134
+ uid: user?.uid,
135
+ });
136
+ await client.query(`delete from site.content_data where content_id=$1`, [contentId]);
137
+ const objectId = (ctname === 'pages' || ['single', 'pages'].includes(type)) && id ? id : cid;
138
+ await client.query(`delete from site.content_data where object_id=$1`, [objectId]);
139
+ await Promise.all(keys.map(async key => dataInsert({
140
+ pg: client,
141
+ table: 'site.content_data',
142
+ data: {
143
+ field_key: key,
144
+ content_id: objectId,
145
+ object_id: objectId,
146
+ field_type: types[key] || 'text',
147
+ field_value: inputTypes[types[key] || ''] === 'json' || key === 'meta' ? undefined : body[key],
148
+ field_value_object: inputTypes[types[key] || ''] === 'json' || key === 'meta' ? body[key] : undefined,
149
+ },
150
+ uid: user?.uid,
151
+ })));
152
+
153
+ // if pages allow localization, update localization
154
+ const localeRes = await updateLocalization(client, id, body, type === 'pages' ? type : ctid1, user?.uid);
155
+
156
+ if (Object.hasOwn(body, 'tag_list')) {
157
+ await client.query('delete from site.tag_data where data_id=$1', [id]);
158
+ if (body.tag_list?.length) {
159
+ await Promise.all(body.tag_list.map(async tag => dataInsert({
160
+ pg: client,
161
+ table: 'site.tag_data',
162
+ data: {
163
+ tag_id: tag,
164
+ data_id: id,
165
+ },
166
+ uid: user?.uid,
167
+ })));
168
+ Object.assign(res, { tag_list: body.tag_list });
169
+ }
170
+ }
171
+
172
+ await client.query('commit');
173
+
174
+ return {
175
+ id, ...res || {}, ...(keys || []).reduce((acc, curr) => ({ ...acc, [curr]: body[curr] }), {}), ...(localeRes || {})
176
+ };
177
+ } catch (err) {
178
+ await client.query('rollback');
179
+ return reply.status(500).send(err.toString());
180
+ } finally {
181
+ client.release();
182
+ }
183
+ }
184
+
185
+ if (!table && !dbtable) {
186
+ return reply.status(400).send('invalid params: type');
187
+ }
188
+
189
+ const client = await pg.connect();
190
+
191
+ try {
192
+ await client.query('begin');
193
+
194
+ const result = await dataUpdate({
195
+ pg: client,
196
+ id,
197
+ table: 'data.' + `"${(table || dbtable)}"`,
198
+ data: body,
199
+ referer: headers?.referer,
200
+ uid: user?.uid,
201
+ }).catch(err => {
202
+ if (err.message?.includes?.('unique constraint')) {
203
+ throw new Error('Порушенні унікальності: ' + err.message?.match?.(/([^"]+)/g)?.[1]);
204
+ }
205
+ throw err;
206
+ });
207
+
208
+ await updateLocalization(client, result?.id, body, ctid, user?.uid);
209
+
210
+ if (Object.hasOwn(body, 'tag_list')) {
211
+ await client.query('delete from site.tag_data where data_id=$1', [id]);
212
+ if (body.tag_list?.length) {
213
+ await Promise.all(body.tag_list.map(async tag => dataInsert({
214
+ pg: client,
215
+ table: 'site.tag_data',
216
+ data: {
217
+ tag_id: tag?.id,
218
+ data_id: id,
219
+ },
220
+ uid: user?.uid,
221
+ })));
222
+ Object.assign(result, { tag_list: body.tag_list });
223
+ }
224
+ }
225
+
226
+ await client.query('commit');
227
+
228
+ if (!result?.id) {
229
+ return reply.status(404).send('content not found');
230
+ }
231
+
232
+ return reply.status(200).send(result);
233
+ } catch (err) {
234
+ await client.query('rollback');
235
+ return reply.status(500).send(err.toString());
236
+ } finally {
237
+ client.release();
238
+ }
112
239
  }
@@ -1,66 +1,79 @@
1
- import path from 'node:path';
2
- import { mkdir, rename } from 'node:fs/promises';
3
-
4
- import { config, getFolder, dataInsert, pgClients } from "@opengis/fastify-table/utils.js";
5
-
6
- import { getMimeType, getFileType, uploadMultiPart } from '@opengis/fastify-file/utils.js';
7
- import { existsSync } from 'node:fs';
8
-
9
- const rootDir = getFolder(config, 'local');
10
-
11
- export default async function uploadMedia(req, reply) {
12
- const { pg = pgClients.client, user = {}, body = {}, query = {} } = req;
13
-
14
- if (!pg.pk?.['site.media']) {
15
- return reply.status(404).send('table not found');
16
- }
17
-
18
- if (query.subdir && (typeof query.subdir !== 'string' || query.subdir.includes('..'))) {
19
- return reply.status(403).send('invalid query params: subdir');
20
- }
21
-
22
- // upload assets
23
- if (req.headers['content-type']?.split?.(';')?.shift?.() === 'multipart/form-data') {
24
- const file = await uploadMultiPart(req);
25
- const filename = `${file.newFilename}${path.extname(file.filepath)}`;
26
- const mime = getMimeType(file.filepath);
27
- const filetype = getFileType(file.filepath);
28
-
29
- const newfilepath = path.join(rootDir, 'media', query.subdir || '', filename);
30
- await mkdir(path.dirname(newfilepath), { recursive: true });
31
- await rename(file.filepath, newfilepath);
32
-
33
- const relpath = path.join('/media', query.subdir || '', filename).replace(/\\/g, '/');
34
- const id = await dataInsert({
35
- pg,
36
- table: 'site.media',
37
- data: {
38
- filename: file.originalFilename,
39
- filetype,
40
- url: relpath,
41
- mime,
42
- filesize: file.size,
43
- },
44
- uid: user?.uid || 0,
45
- }).then(el => el?.rows?.[0]?.media_id);
46
-
47
- return reply.status(200).send({
48
- relpath: path.join('/media', query.subdir || '').replace(/\\/g, '/'),
49
- id,
50
- filename,
51
- filetype,
52
- type: 'file',
53
- });
54
- }
55
-
56
- // create directory
57
- const relpath = path.join('/media', query.subdir).replace(/\\/g, '/');
58
- const dirpath = path.join(rootDir, relpath);
59
- await mkdir(dirpath, { recursive: true });
60
-
61
- return reply.status(200).send({
62
- relpath,
63
- dirname: path.basename(query.subdir),
64
- type: 'dir',
65
- });
1
+ import path from 'node:path';
2
+ import { mkdir } from 'node:fs/promises';
3
+
4
+ import { uploadMultiPart, config, getFolder, dataInsert, pgClients } from "@opengis/fastify-table/utils.js";
5
+
6
+ const rootDir = getFolder(config, 'local');
7
+ const dir = '/files';
8
+
9
+ export default async function uploadMedia(req, reply) {
10
+ const { pg = pgClients.client, user = {}, query = {} } = req;
11
+
12
+ if (!pg?.pk?.['site.media']) {
13
+ return reply.status(404).send('table not found');
14
+ }
15
+
16
+ if (query.subdir && (typeof query.subdir !== 'string' || query.subdir.includes('..'))) {
17
+ return reply.status(403).send('invalid query params: subdir');
18
+ }
19
+
20
+ // upload assets
21
+ if (req.headers['content-type']?.split?.(';')?.shift?.() === 'multipart/form-data') {
22
+ const file = await uploadMultiPart(req, { subdir: query.subdir || '', originalFilename: true }).catch(err => {
23
+ if (err.message === 'file with specified name already exists in directory') {
24
+ err.message = 'Файл з вказаною назвою вже існує';
25
+ err.statusCode = 400;
26
+ }
27
+ throw err;
28
+ });
29
+
30
+ const { originalFilename: filename, filetype, mimetype } = file;
31
+ const relpath = path.join(dir, query.subdir || '', file.originalFilename).replace(/\\/g, '/');
32
+
33
+ const id = await dataInsert({
34
+ pg,
35
+ table: 'site.media',
36
+ data: {
37
+ filename,
38
+ filetype,
39
+ subdir: query.subdir,
40
+ url: relpath,
41
+ mime: mimetype,
42
+ filesize: file.size,
43
+ },
44
+ uid: user?.uid,
45
+ }).then(el => el?.rows?.[0]?.media_id);
46
+
47
+ return reply.status(200).send({
48
+ res: 'ok',
49
+ name: filename,
50
+ type: 'file',
51
+ mimetype,
52
+ result: {
53
+ file_id: id,
54
+ format: file.extension,
55
+ size: file.size,
56
+ // entity_id: resultInsert?.entity_id,
57
+ file_path: relpath,
58
+ file_name: filename,
59
+ dir: path.dirname(relpath).replace(/\\/g, '/'),
60
+ native_file_name: filename,
61
+ },
62
+ });
63
+ }
64
+
65
+ if (!query.subdir) {
66
+ return reply.status(400).send('not enough query params: subdir');
67
+ }
68
+
69
+ // create directory
70
+ const relpath = path.join(dir, query.subdir).replace(/\\/g, '/');
71
+ const dirpath = path.join(rootDir, relpath);
72
+ await mkdir(dirpath, { recursive: true });
73
+
74
+ return reply.status(200).send({
75
+ relpath,
76
+ dirname: path.basename(query.subdir),
77
+ type: 'dir',
78
+ });
66
79
  }
@@ -1,3 +1,5 @@
1
+ import { config } from '@opengis/fastify-table/utils.js';
2
+
1
3
  // perimissions
2
4
  import getPermissions from './controllers/getPermissions.js';
3
5
  import setPermissions from './controllers/setPermissions.js';
@@ -11,35 +13,102 @@ import download from './controllers/downloadMedia.js';
11
13
 
12
14
  // content
13
15
  import getContent from './controllers/getContent.js';
16
+ import getContentBySlug from './controllers/getContentBySlug.js';
14
17
  import insertContent from './controllers/insertContent.js';
15
18
  import updateContent from './controllers/updateContent.js';
16
19
  import deleteContent from './controllers/deleteContent.js';
20
+ import { translateContent, translateCollection } from './controllers/translate.js';
21
+
22
+ // suggest
23
+ import cmsSuggest from './controllers/cmsSuggest.js';
24
+
25
+ // statistics
26
+ import cmsStat from './controllers/cmsStat.js';
27
+ import searchContent from './controllers/searchContent.js';
28
+
29
+ import getAppSettings from "./controllers/properties.get.js";
30
+ import postAppSettings from "./controllers/properties.post.js";
31
+
32
+ const suggestSchema = {
33
+ querystring: {
34
+ type: 'object',
35
+ properties: {
36
+ search: { type: 'string' },
37
+ filter: { type: 'string' },
38
+ limit: { type: 'number', exclusiveMinimum: 0 },
39
+ },
40
+ },
41
+ };
17
42
 
18
- const schemaObj = {
19
- type: 'object',
43
+ const propertiesSchema = {
44
+ type: "object",
20
45
  properties: {
21
46
  params: {
22
- type: 'string',
23
- enum: ['preview', 'download'],
24
- }
25
- }
47
+ id: { type: "string", pattern: "^([\\d\\w]+)$" },
48
+ key: { type: "string", pattern: "^([\\d\\w._]+)$" },
49
+ },
50
+ querystring: {
51
+ json: { type: "string", pattern: "^([\\d\\w]+)$" },
52
+ },
53
+ },
26
54
  };
27
55
 
28
- const params = { config: { policy: ['public'] } };
56
+ const translateContentSchema = {
57
+ type: "object",
58
+ properties: {
59
+ params: {
60
+ id: {
61
+ type: "string"
62
+ },
63
+ },
64
+ querystring: {
65
+ required: ['to'],
66
+ additionalProperties: false,
67
+ from: {
68
+ type: "string"
69
+ },
70
+ to: {
71
+ type: "string"
72
+ },
73
+ },
74
+ },
75
+ };
76
+
77
+ const params = { config: { policy: 'L0' } };
29
78
 
30
79
  export default async function route(app) {
31
- app.post('/cms-permissions/:id', params, setPermissions);
80
+ app.post('/cms-permissions/:id', { config: { role: 'admin' } }, setPermissions);
32
81
  app.get('/cms-permissions/:id?', params, getPermissions);
82
+ app.get('/cms-translate', { config: { role: "admin" }, schema: translateContentSchema }, translateContent);
83
+ app.get('/cms-translate-collection/:id', { config: { role: "admin" }, schema: translateContentSchema }, translateCollection);
33
84
 
34
85
  app.post('/cms-media/upload', params, uploadMedia);
35
86
  app.get('/cms-media', params, listMedia);
36
- app.get('/cms-media/:id/file', { ...params, schema: schemaObj }, download);
87
+ app.get('/cms-media/:id/:type', params, download);
37
88
  app.get('/cms-media/:id', params, metadata);
38
- app.delete('/cms-media/:id', params, del);
89
+ app.delete('/cms-media/:id?', params, del); // media file with id / subdir without
39
90
  app.get('/cms-media/:id/delete', params, del); // debug
40
91
 
41
- app.get('/cms/:type/:id?', params, getContent);
92
+ // Bearer token required
93
+ app.get('/cms-content/:slug/:lang?', params, getContentBySlug);
94
+ app.get('/cms-search', params, searchContent);
95
+ app.get('/cms/:type', params, getContent);
96
+ app.get('/cms/:type/:id/:lang?', params, getContent);
97
+
42
98
  app.post('/cms/:type/:id?', params, insertContent);
43
- app.put('/cms/:type/:id', params, updateContent);
99
+ app.put('/cms/:type/:id?', params, updateContent);
44
100
  app.delete('/cms/:type/:id', params, deleteContent);
101
+
102
+ app.get('/cms-suggest/:id', { ...params, schema: suggestSchema }, cmsSuggest);
103
+ app.get('/cms-stat', params, cmsStat);
104
+ app.get(
105
+ "/settings",
106
+ { config: { policy: "L0" }, schema: propertiesSchema },
107
+ getAppSettings
108
+ );
109
+ app.post(
110
+ "/settings",
111
+ { config: { policy: "L0" } },
112
+ postAppSettings
113
+ );
45
114
  }
@@ -0,0 +1,36 @@
1
+ export default async function additionalData(pg, rows, locale, fields = '*') {
2
+ const { rows: localizations = [] } = await pg.query(`select json_object_agg(field_key, field_value), object_id from site.localization
3
+ where object_id=any($1) and ${locale && locale !== 'uk' ? 'REVERSE(split_part(REVERSE(field_key), \':\', 1)) = $2' : '1=1'} group by object_id`, [rows.map(el => el.id), locale && locale !== 'uk' ? locale : null].filter(Boolean));
4
+
5
+ const { rows: tagsList = [] } = await pg.query(`
6
+ SELECT
7
+ td.data_id,
8
+ json_agg(
9
+ json_build_object('id', t.tag_id, 'text', t.value, 'color', t.color, 'slug', t.slug, 'locale', t.locale)
10
+ ) AS tag
11
+ FROM site.tag_data td
12
+ JOIN site.tags t ON t.tag_id = td.tag_id
13
+ where data_id=any($1)
14
+ GROUP BY td.data_id;
15
+ `, [rows.map(el => el.id)]);
16
+
17
+ rows.forEach(row => {
18
+ if (locale && locale !== 'uk' && row.meta && Object.keys(row.meta || {}).find(key => key.split(':').pop() === locale)) {
19
+ row.meta = Object.fromEntries(Object.keys(row.meta || {}).filter(key => key.split(':').pop() === locale && row.meta?.[key]).map(key => [key.split(':').shift(), row.meta[key]]));
20
+ }
21
+
22
+ const localization = localizations.find(el => el.object_id === row.id);
23
+ const localizationKeys = Object.keys(localization?.json_object_agg || {}).filter(key => row[key.split(':').shift()]);
24
+ const localizationObj1 = Object.entries(localization?.json_object_agg || {}).filter(([key]) => rows.length > 1 ? true : localizationKeys.includes(key)).reduce((acc, curr) => ({ ...acc, [curr[0]]: typeof curr[1] === 'string' && curr[1].startsWith('[') && curr[1].endsWith(']') ? JSON.parse(curr[1]) : curr[1] }), {});
25
+ const localizationObj = fields?.length ? Object.fromEntries(Object.entries(localizationObj1).filter(([key]) => fields.split(',').includes(key.split(':').shift()))) : localizationObj1;
26
+ if (locale && locale !== 'uk') {
27
+ Object.assign(row, Object.keys(localizationObj).reduce((acc, curr) => ({ ...acc, [curr.replace(`:${locale}`, '')]: localizationObj[curr] }), {}));
28
+ } else if (!locale) {
29
+ Object.assign(row, localizationObj);
30
+ }
31
+
32
+ const { tag = [] } = tagsList.find(el => el?.data_id === row?.id) || {};
33
+ const localizedTags = locale ? tag.map(el => ({ ...el, text: el.locale?.[locale] || el.text })) : tag;
34
+ Object.assign(row, { tag_list: localizedTags });
35
+ });
36
+ }