@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
@@ -0,0 +1,82 @@
1
+ import { pgClients, getData, getMeta } from "@opengis/fastify-table/utils.js";
2
+
3
+ import additionalData from "./additionalData.js";
4
+
5
+ export default async function getCollection({
6
+ table, id, limit, maxLimit, order, search, tags, filter, state, page, desc, sql, locale, contextQuery, statusQuery, columns, preview, user, defaultColumns, fields, defaultFields = [], defaultFilters = []
7
+ }, reply, pg = pgClients.client) {
8
+ if (!table || !pg.pk?.['data.' + table]) {
9
+ return { message: 'content table not found', status: 404 };
10
+ }
11
+
12
+ const isPinExists = await pg.query(`
13
+ SELECT 1
14
+ FROM pg_catalog.pg_attribute a
15
+ JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
16
+ JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid
17
+ WHERE n.nspname = 'data'
18
+ AND c.relname = $1
19
+ AND a.attname = 'is_pin'
20
+ AND a.attnum > 0
21
+ AND NOT a.attisdropped;
22
+ `, [table]).then(el => el.rowCount);
23
+
24
+ const defaultOrder = `case when status='archived' then true else false end ${isPinExists ? ', is_pin desc' : ''}, published_at desc nulls last`;
25
+
26
+ const localeQuery = locale && locale !== 'uk' ? `id in ( select object_id from site.localization where REVERSE(split_part(REVERSE(field_key), ':', 1)) = '${locale.replace(/'/g, "''")}' )` : undefined;
27
+ const tagQuery = tags ? `id in (SELECT data_id FROM site.tag_data td left join site.tags ts on td.tag_id=ts.tag_id WHERE ts.slug = any('{ ${tags.replace(/'/g, "''")} }'::text[]) or ts.tag_id = any('{ ${tags.replace(/'/g, "''")} }'::text[]))` : null;
28
+ const cQuery = [id ? `'${id}' in (id,slug)` : contextQuery, localeQuery, ((columns || []).concat(defaultColumns || [])).find(col => col.name === 'status') ? statusQuery : null, tagQuery].filter(Boolean).join(' and ');
29
+
30
+ const columnList = await getMeta({
31
+ pg,
32
+ table: 'data.' + `"${table}"`
33
+ }).then(el => el.columns?.map?.(col => col.name) || []);
34
+ const existingFields = fields && !id ? defaultFields.concat((fields || '').split(',')).filter(colName => columnList.includes(colName)).join(',') : null;
35
+
36
+ const filterList = defaultFilters.concat(columns.map(col => ({ name: col.name, type: col.type && ['date', 'datetime'].includes(col.type) ? 'Date' : 'Text', label: col.label, sql: col.sql })));
37
+
38
+ const res = await getData({
39
+ pg,
40
+ table: 'data.' + `"${table}"`,
41
+ columns: existingFields,
42
+ filterList,
43
+ query: {
44
+ filter,
45
+ state,
46
+ limit: Math.min(limit, maxLimit),
47
+ page,
48
+ search,
49
+ order: order ? order : defaultOrder,
50
+ desc,
51
+ sql
52
+ },
53
+ user,
54
+ contextQuery: cQuery,
55
+ }, reply, true);
56
+
57
+ const locales = await pg.query(`select array_agg(distinct REVERSE(split_part(REVERSE(field_key), ':', 1))) from site.localization where object_id in (select id from ${'data.' + `"${table}"`})`).then(el => el.rows?.[0]?.array_agg || []);
58
+
59
+ // Apply localization and tags etc.
60
+ if (res?.rows?.length) {
61
+ await additionalData(pg, res.rows, locale, id ? null : existingFields);
62
+ };
63
+
64
+ const columns1 = !id
65
+ ? columns?.map(col => Object.assign(col, { type: col.name === 'published_at' ? 'date' : col.type }))?.filter?.(el => !el.hidden)
66
+ : columns?.filter?.(el => !el.hidden);
67
+
68
+ const finalColumns = (defaultColumns || []).concat(columns1.filter(col => defaultColumns.findIndex(el => el.name === col.name) === -1));
69
+
70
+ finalColumns.filter(el => el.default).forEach(col => {
71
+ const { name, localization } = columns1.find(item => item.name === col.name) || {};
72
+ if (name) {
73
+ Object.assign(col, { localization });
74
+ }
75
+ });
76
+
77
+ if (res?.columns) {
78
+ Object.assign(res, { type: 'collection', locales, preview_path: preview, columns: finalColumns });
79
+ }
80
+
81
+ return res;
82
+ }
@@ -0,0 +1,188 @@
1
+ import path from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+
4
+ import { pgClients, metaFormat, getFilterSQL, getFilter, getMeta } from '@opengis/fastify-table/utils.js';
5
+
6
+ const inputTypes = existsSync('input-types.json') ? JSON.parse(readFileSync('input-types.json') || '{}') : {};
7
+
8
+ import additionalData from "./additionalData.js";
9
+
10
+ export default async function getSingle({
11
+ contentId: contentId1, id: id1, limit: queryLimit, maxLimit, search, filter, locale, statusQuery, user, sql, page, defaultColumns = [], fields, defaultFields = [], defaultFilters = [],
12
+ }, pg = pgClients.client) {
13
+ const id = id1 || (contentId1 === 'pages' ? undefined : contentId1) || '';
14
+ const contentId = id1 ? contentId1 : "pages";
15
+
16
+ // defaultColumns.push({
17
+ // "name": "content_type_id",
18
+ // "label": "Тип сторінки",
19
+ // "type": "select",
20
+ // "data": "cms.page_type"
21
+ // });
22
+
23
+ const customColumns = await pg.query(`
24
+ SELECT columns FROM site.content_types WHERE content_type_id in (select content_type_id from site.contents where content_id=$1) limit 1
25
+ `, [id1 || contentId]).then(el => el.rows?.[0]?.columns || []);
26
+
27
+ const contentQuery = contentId === 'pages' || !id
28
+ ? `(type = 'single' or content_type_id = 'pages')`
29
+ : 'content_type_id=(select content_type_id from site.contents where content_id=$2)';
30
+
31
+ const columns = defaultColumns.concat(customColumns.filter(col => defaultColumns.findIndex(el => el.name === col.name) === -1));
32
+
33
+ columns.filter(el => el.default).forEach(col => {
34
+ const { name, localization } = customColumns.find(item => item.name === col.name) || {};
35
+ if (name) {
36
+ Object.assign(col, { localization });
37
+ }
38
+ });
39
+
40
+ const limit = Math.min(maxLimit, +(queryLimit || 20));
41
+ const offset = page && page > 0 && !id ? (page - 1) * limit : 0;
42
+
43
+ const fData = filter
44
+ ? await getFilterSQL({
45
+ pg,
46
+ table: 'site.contents',
47
+ filter,
48
+ search,
49
+ filterList: defaultFilters,
50
+ query: `
51
+ content_type_id IN (
52
+ SELECT content_type_id
53
+ FROM site.content_types
54
+ WHERE ${contentQuery || 'true'}
55
+ )
56
+ `,
57
+ })
58
+ : {
59
+ optimizedSQL: 'SELECT * FROM site.contents WHERE true',
60
+ };
61
+
62
+ const localeQuery = locale && locale !== 'uk' && pg.pk?.['site.contents'] ? `${pg.pk?.['site.contents']} in ( select object_id from site.localization where REVERSE( split_part( REVERSE(field_key), ':', 1) ) = '${locale.replace(/'/g, "''")}' )` : undefined;
63
+
64
+ const columnList = await getMeta({
65
+ pg,
66
+ table: 'site.contents',
67
+ }).then(el => el.columns?.map?.(col => col.name) || []);
68
+ const existingFields = fields && contentId === 'pages' || !id ? defaultFields.concat((fields || '').split(',')).filter(colName => columnList.includes(colName)).concat(['content_id AS id']).join(',') : '';
69
+ const extraKeys = fields ? fields.split(',').filter(key => !defaultFields.includes(key)) : [];
70
+ const cols = existingFields.length ? existingFields.concat(extraKeys?.length ? `,"extraKeys"` : []) : `content_id AS id,
71
+ content_id,
72
+ created_by,
73
+ title,
74
+ slug,
75
+ meta,
76
+ published_by,
77
+ published_at,
78
+ status,
79
+ content_type_id,
80
+ /* content_type_id AS type, */
81
+ main_image
82
+ ${contentId === 'pages' || !id ? `,"extraKeys"` : ''}`;
83
+
84
+ const q = `
85
+ SELECT ${cols} FROM (${fData.optimizedSQL}) a
86
+ ${extraKeys || id ? `LEFT JOIN LATERAL (
87
+ SELECT json_agg(row_to_json(q)) AS "extraKeys"
88
+ FROM (
89
+ SELECT field_key, field_type, field_value, field_value_object
90
+ FROM site.content_data
91
+ WHERE content_id = a.content_id
92
+ ) q
93
+ ) b ON true` : ''}
94
+
95
+ LEFT JOIN LATERAL (
96
+ SELECT type FROM site.content_types WHERE content_type_id=a.content_type_id LIMIT 1
97
+ )c ON true
98
+
99
+ WHERE $1=$1 and $2=$2
100
+ and ${statusQuery || 'true'}
101
+ and ${localeQuery || '1=1'}
102
+ and ${contentQuery || 'true'}
103
+ and ${id ? `$2 in (slug, content_id)` : 'true'}
104
+ order by case when status='archived' then true else false end, published_at desc nulls last
105
+ LIMIT ${limit} OFFSET ${id ? 0 : offset}
106
+ `;
107
+
108
+ if (sql && user?.user_type?.includes?.('admin')) {
109
+ return q; // Expose raw SQL for debugging
110
+ }
111
+
112
+ const { total = 0, filtered = 0 } = await pg.query(`
113
+ SELECT COUNT(*)::int as total,
114
+ count(*) FILTER(WHERE ${fData.q || "true"})::int as filtered
115
+ FROM site.contents
116
+ WHERE content_type_id IN (
117
+ SELECT content_type_id
118
+ FROM site.content_types
119
+ WHERE ${localeQuery || '1=1'}
120
+ and $1=$1 and $2=$2 and ${contentQuery || 'true'}
121
+ )
122
+ and ${statusQuery || 'true'}
123
+ `, [contentId, id]).then(el => el.rows?.[0] || {});
124
+
125
+ const { rows = [] } = await pg.query(q, [contentId, id || '']);
126
+
127
+ const locales = await pg.query(`select array_agg(distinct REVERSE(split_part(REVERSE(field_key), ':', 1))) from site.localization
128
+ where object_id in (select content_id from site.contents
129
+ where content_type_id IN (
130
+ SELECT content_type_id FROM site.content_types where $1=$1 and $2=$2 and ${contentQuery || 'true'}
131
+ )
132
+ and ${statusQuery || 'true'}
133
+ )`, [contentId, id]
134
+ ).then(el => el.rows?.[0]?.array_agg || []);
135
+
136
+ // Flatten extra fields into main object
137
+ const rows1 = rows.map(row => ({
138
+ ...row,
139
+ ...Object.fromEntries(
140
+ row.extraKeys?.filter(k => extraKeys?.length ? extraKeys.includes(k.field_key) : true)?.map(k =>
141
+ [k.field_key, inputTypes[k.field_type] === 'json' || k.field_key === 'meta'
142
+ ? k.field_value_object
143
+ : k.field_value]
144
+ ) || []
145
+ ),
146
+ extraKeys: undefined,
147
+ }));
148
+
149
+ // Apply localization and tags etc.
150
+ await additionalData(pg, rows1, locale, contentId === 'pages' || !id ? [existingFields, extraKeys].filter(Boolean).join(',') : null);
151
+
152
+ // Apply metadata formatting
153
+ await metaFormat({
154
+ rows: rows1,
155
+ cls: {
156
+ created_by: 'core.user_mentioned',
157
+ // content_type_id: 'cms.type_id',
158
+ // type: 'cms.type_id',
159
+ },
160
+ sufix: false,
161
+ }, pg);
162
+
163
+ const { list: filters = [] } = await getFilter({
164
+ pg, table: 'single.default.table', user, filter: `content_type_id in (select content_type_id from site.content_types where type='single' or content_type_id='pages')`
165
+ }) || {};
166
+
167
+ const finalColumns = !id
168
+ ? columns?.map?.(col => Object.assign(col, { type: col.name === 'published_at' ? 'date' : col.type }))?.filter?.(el => !el.hidden)
169
+ : columns?.filter?.(el => !el.hidden);
170
+
171
+ rows1.filter(row => row.single_sections?.length).forEach(row => {
172
+ row.single_sections.filter(section => section.documents?.length).forEach(section => {
173
+ section.documents.filter(doc => doc.file).forEach(doc => Object.assign(doc, { file_name: path.basename(doc.file) }));
174
+ });
175
+ });
176
+
177
+ return {
178
+ locales,
179
+ type: 'single',
180
+ total,
181
+ filtered,
182
+ count: rows1.length,
183
+ pk: 'id',
184
+ rows: rows1,
185
+ columns: finalColumns,
186
+ filters,
187
+ };
188
+ }
@@ -0,0 +1,87 @@
1
+ import { dataInsert, pgClients } from "@opengis/fastify-table/utils.js";
2
+
3
+ import requestTranslation from "./requestTranslation.js";
4
+
5
+ export default async function insertContentLocalization({ send = () => { }, table, id, from, to, nocache, schemaKeys, user }, pg = pgClients.client) {
6
+ if (!to) {
7
+ const resp = { error: 'not enough query params: to', code: 400 };
8
+ send(resp.error + ': ' + id);
9
+ return resp;
10
+ }
11
+
12
+ const row = await pg.query(`select * ${table ? '' : ',content_id as id'} from ${table ? `data."${table}"` : 'site.contents'} where $1 in (id,slug)`, [id]).then(el => el.rows?.[0]);
13
+
14
+ if (!row) {
15
+ const resp = { error: 'content not found', code: 404 };
16
+ send(resp.error + ': ' + id);
17
+ return resp;
18
+ }
19
+
20
+ const localizationExists = await pg.query('select 1 from site.localization where object_id=$1 and field_key is not null and split_part(field_key,\':\',2)=$2', [row.id, to])
21
+ .then(el => el.rowCount);
22
+
23
+ if (localizationExists && !nocache) {
24
+ const resp = { error: 'target localization already exists', code: 400 };
25
+ send(resp.error + ': ' + id);
26
+ return resp;
27
+ }
28
+
29
+ const localization = await pg.query('select json_object_agg(split_part(field_key,\':\',2),field_value) from site.localization where object_id=$1 and field_key is not null and split_part(field_key,\':\',2)=$2', [row.id, from])
30
+ .then(el => el.rows?.[0]?.json_object_agg || {});
31
+
32
+ if (!row) {
33
+ const resp = { error: 'content not found', code: 404 };
34
+ send(resp.error + ': ' + id);
35
+ return resp;
36
+ }
37
+
38
+ const obj = { ...row, ...localization };
39
+
40
+ const entries = Object.entries(obj).filter(([key, value]) => value && schemaKeys.includes(key) && (typeof value === 'string' || (Array.isArray(value) && value?.[0] && typeof value?.[0] === 'object' && Object.keys(value).length)));
41
+ const skipped = schemaKeys.filter(key => !entries.map(([el]) => el).includes(key));
42
+
43
+ const { result, error, code = 200 } = await requestTranslation(entries, from, to);
44
+
45
+ if (error && code) {
46
+ send(error + ': ' + id);
47
+ return { error, code };
48
+ }
49
+
50
+ const arr = Object.keys(result).reduce((acc, curr) => {
51
+ acc.push({
52
+ field_key: curr,
53
+ field_value: result[curr],
54
+ object_id: id,
55
+ });
56
+ return acc;
57
+ }, []);
58
+
59
+ const client = await pg.connect();
60
+
61
+ try {
62
+ await client.query('begin');
63
+
64
+ if (nocache) {
65
+ const deleted = await client.query('delete from site.localization where object_id=$1 and split_part(field_key,\':\',2)=$2', [id, to]).then(el => el.rowCount || 0);
66
+ send(`deleted existing localizations: ${deleted}, ${id}`);
67
+ console.log('deleted existing localizations: ', deleted, id);
68
+ }
69
+
70
+ await Promise.all(arr.map(async row => dataInsert({
71
+ pg: client,
72
+ table: 'site.localization',
73
+ data: row,
74
+ uid: user?.uid,
75
+ })));
76
+ await client.query('commit');
77
+
78
+ send(`translation success: ${table}/${id}`);
79
+ return { table, id: row.id, keys: schemaKeys, skipped, to };
80
+ } catch (err) {
81
+ await client.query('rollback');
82
+ send(`translation error: ${err.toString()} (${id})`);
83
+ return { error: err.toString(), code: 500 };
84
+ } finally {
85
+ client.release();
86
+ }
87
+ }
@@ -0,0 +1,85 @@
1
+ import { config } from "@opengis/fastify-table/utils.js";
2
+
3
+ const { host = 'https://translate.softpro.ua', key = '' } = config.integrations?.translation || {};
4
+
5
+ const divider = ' <divider> ';
6
+
7
+ // Wrap dot notation keys in quotes
8
+ export const arrayToPhrases = (arr, prefix = 'item') => {
9
+ if (!arr || !arr.length) return [];
10
+ return arr.flatMap((obj, idx) =>
11
+ Object.entries(obj).map(([key, value]) => ({
12
+ prefix,
13
+ value,
14
+ // phrase: `"${prefix}"."${key}".${idx}: ${value}`,
15
+ key,
16
+ idx
17
+ }))
18
+ );
19
+ };
20
+
21
+ export default async function requestTranslation(entries, from = 'uk', to) {
22
+ // Flatten all phrases
23
+ const phrasesWithMeta = entries.flatMap(([key, value]) =>
24
+ Array.isArray(value) ? arrayToPhrases(value, key) : [{ key, value, prefix: key }]
25
+ );
26
+
27
+ // Translate each phrase independently
28
+ const translatedPhrases = await Promise.all(
29
+ phrasesWithMeta.map(async ({ key, idx, prefix, value }) => {
30
+ if (!value || typeof value === 'number' || value?.startsWith?.('/files/')) {
31
+ return { key, idx, prefix, value, skip: true }; // fallback to original
32
+ }
33
+
34
+ try {
35
+ const parts = value.startsWith('<') && value.endsWith('>')
36
+ ? [...value.matchAll(/(<[^>]+>)([^<]*)(<\/[^>]+>)/g)].map(([, openTag, str, closeTag]) => ({ openTag, str, closeTag }))
37
+ : [{ str: value }];
38
+
39
+ const q = parts.map(({ str }) => str).join(divider);
40
+
41
+ const resp = await fetch(`${host}/translate`, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify({
45
+ q,
46
+ source: from || "auto",
47
+ target: to,
48
+ format: "text",
49
+ api_key: key
50
+ }),
51
+ });
52
+
53
+ if (resp.status !== 200 || resp.headers.get('content-type') !== 'application/json') {
54
+ const err = await resp.text();
55
+ console.warn('translation request error', resp.status, err);
56
+ return { key, idx, prefix, value, error: err }; // fallback to original
57
+ }
58
+
59
+ const body = await resp.json();
60
+ const resultValue = body.translatedText ? parts.map(({ openTag, str, closeTag }, i) => `${openTag || ''}${body.translatedText.split(divider)[i] || str || ''}${closeTag || ''}`).join(' ') : null;
61
+ return { key, idx, prefix, value: resultValue || value };
62
+ } catch (err) {
63
+ console.warn('translation request failed', err.toString());
64
+ return { key, idx, prefix, value, error: err.toString() }; // fallback
65
+ }
66
+ })
67
+ );
68
+
69
+ const result = translatedPhrases.reduce((acc, { key, idx, prefix, value }) => {
70
+ if (!acc[`${prefix}:${to}`]) acc[`${prefix}:${to}`] = Array.isArray(entries.find(e => e[0] === prefix)[1]) ? [] : null;
71
+
72
+ if (Array.isArray(entries.find(e => e[0] === prefix)[1])) {
73
+ if (!acc[`${prefix}:${to}`][idx]) acc[`${prefix}:${to}`][idx] = {};
74
+ acc[`${prefix}:${to}`][idx][key] = value;
75
+ } else {
76
+ acc[`${prefix}:${to}`] = value;
77
+ }
78
+
79
+ return acc;
80
+ }, {});
81
+
82
+ const errors = translatedPhrases.filter(({ error }) => !!error).length;
83
+
84
+ return { result, errors };
85
+ }
@@ -0,0 +1,48 @@
1
+ import { dataInsert, getTemplate } from "@opengis/fastify-table/utils.js";
2
+
3
+ export default async function updateLocalization(pg, id, body, contentTypeId, uid) {
4
+ if (!pg || !id || !body || !contentTypeId) { return null };
5
+
6
+ const 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, contentTypeId]).then(el => el.rows?.[0]?.columns || []);
7
+
8
+ // const contentColumns = await pg.query('select columns from site.content_types where content_type_id=$1', [contentTypeId])
9
+ // .then(el => el.rows?.[0]?.columns || []);
10
+
11
+ const loadTable = contentTypeId === 'pages' ? await getTemplate('table', 'single.default.table') : {};
12
+
13
+ const columns = contentTypeId === 'pages'
14
+ ? (loadTable?.columns || []).concat(contentColumns.filter(col => loadTable?.columns.findIndex(el => el.name === col.name) === -1))
15
+ : contentColumns;
16
+
17
+ const locales = await pg.query('select locales from site.spaces where space_id = $1 limit 1', ['default']).then(el => el.rows?.[0]?.locales || []);
18
+
19
+ await pg.query('delete from site.localization where object_id=$1', [id]);
20
+
21
+ // localization disable for current space
22
+ if (!locales?.length) { return null; }
23
+
24
+ const schemaKeys = columns.filter(el => el?.name && el.localization).map(el => el.name);
25
+
26
+ const bodyKeys = Object.keys(body || {}).filter(key => body[key] && key.includes(':') && key.split(':').pop() && locales.includes(key.split(':').pop()) && schemaKeys.includes(key.split(':').shift()));
27
+ const obj = bodyKeys.reduce((acc, curr) => ({ ...acc, [curr]: body[curr] }), {});
28
+
29
+ if (bodyKeys.length === 0) { return null; }
30
+
31
+ const arr = Object.keys(obj).reduce((acc, curr) => {
32
+ acc.push({
33
+ field_key: curr,
34
+ field_value: body[curr],
35
+ object_id: id,
36
+ });
37
+ return acc;
38
+ }, []);
39
+
40
+ await Promise.all(arr.map(async row => dataInsert({
41
+ pg,
42
+ table: 'site.localization',
43
+ data: row,
44
+ uid,
45
+ })));
46
+
47
+ return obj;
48
+ }
@@ -0,0 +1,26 @@
1
+ import { dataDelete, pgClients } from '@opengis/fastify-table/utils.js';
2
+
3
+ export default async function deleteSpace({ pg = pgClients.client, user, params, headers = {} }, reply) {
4
+ if (!params?.id) { return reply.status(400).send('not enough params: id'); }
5
+ if (!pg?.pk) { return reply.status(400).send('empty pg'); }
6
+ if (!pg?.pk?.['site.spaces']) { return reply.status(400).send('table not found: site.spaces'); }
7
+
8
+ if (params?.id && params.id === 'default') {
9
+ return reply.status(400).send('default space cannot be deleted');
10
+ }
11
+
12
+ const exists = await pg.query('select space_id from site.spaces where space_id=$1', [params.id]).then(el => el.rows?.[0]?.space_id);
13
+ if (!exists) { return reply.status(404).send('space not found: ' + params.id); }
14
+
15
+ const { referer } = headers;
16
+
17
+ const res = await dataDelete({
18
+ pg,
19
+ table: 'site.spaces',
20
+ id: params?.id,
21
+ uid: user?.uid,
22
+ referer,
23
+ });
24
+
25
+ return { id: params.id, ...res || {} };
26
+ }
@@ -0,0 +1,28 @@
1
+ import { pgClients } from '@opengis/fastify-table/utils.js';
2
+
3
+ const maxLimit = 100;
4
+ const defaultLimit = 20;
5
+
6
+ export default async function getSpaces({ pg = pgClients.client, user, params = {}, query = {} }, reply) {
7
+ if (!pg?.pk) { return reply.status(400).send('empty pg'); }
8
+ if (!pg?.pk?.['site.spaces']) { return reply.status(400).send('table not found: site.spaces'); }
9
+
10
+ const limit = Math.min(maxLimit, +(query.limit || defaultLimit));
11
+ const offset = query.page && query.page > 0 && !params.id ? (query.page - 1) * limit : 0;
12
+
13
+ const q = `select * from site.spaces where ${params.id ? 'space_id=$1' : '1=1'} limit ${limit} offset ${offset}`;
14
+ if (query.sql && user?.uid) return q;
15
+
16
+ const { rows = [] } = await pg.query(q, [params.id].filter(Boolean));
17
+
18
+ const total = await pg.queryCache('select count(*) from site.spaces', { table: 'site.spaces' }).then(el => el.rows?.[0]?.count || 0);
19
+
20
+ rows.forEach(row => Object.assign(row, { id: row.space_id }));
21
+
22
+ if (params.id) {
23
+ if (!rows.length) { return reply.status(404).send('space not found: ' + params.id); }
24
+ return reply.status(200).send(rows[0]);
25
+ }
26
+
27
+ return { total, filtered: rows.length, rows };
28
+ }
@@ -0,0 +1,22 @@
1
+ import { dataInsert, pgClients } from '@opengis/fastify-table/utils.js';
2
+
3
+ export default async function insertSpace({ pg = pgClients.client, user, params, headers = {}, body = {} }, reply) {
4
+ if (!pg?.pk) { return reply.status(400).send('empty pg'); }
5
+ if (!pg?.pk?.['site.spaces']) { return reply.status(400).send('table not found: site.spaces'); }
6
+
7
+ const exists = params?.id ? await pg.query('select space_id from site.spaces where space_id=$1', [params.id]).then(el => el.rows?.[0]?.space_id) : false;
8
+ if (exists) { return reply.status(400).send('space aleady exists: ' + params?.id); }
9
+
10
+ const { referer } = headers;
11
+
12
+ const res = await dataInsert({
13
+ pg,
14
+ table: 'site.spaces',
15
+ data: body,
16
+ id: params?.id,
17
+ uid: user?.uid,
18
+ referer,
19
+ });
20
+
21
+ return { id: res?.rows?.[0]?.space_id, rows: res?.rows || [] };
22
+ }
@@ -0,0 +1,24 @@
1
+ import { dataUpdate, pgClients } from '@opengis/fastify-table/utils.js';
2
+
3
+ export default async function updateSpace({ pg = pgClients.client, user, params, headers = {}, body = {} }, reply) {
4
+ // if (!params?.id) { return reply.status(400).send('not enough params: id'); }
5
+ if (!pg?.pk) { return reply.status(400).send('empty pg'); }
6
+ if (!pg?.pk?.['site.spaces']) { return reply.status(400).send('table not found: site.spaces'); }
7
+
8
+ const id = 'default';
9
+ const exists = await pg.query('select space_id from site.spaces where space_id=$1', [id]).then(el => el.rows?.[0]?.space_id);
10
+ if (!exists) { return reply.status(404).send('space not found: ' + id); }
11
+
12
+ const { referer } = headers;
13
+
14
+ const res = await dataUpdate({
15
+ pg,
16
+ table: 'site.spaces',
17
+ data: body,
18
+ id,
19
+ uid: user?.uid,
20
+ referer,
21
+ });
22
+
23
+ return { id, ...res || {} };
24
+ }
@@ -0,0 +1,20 @@
1
+ // space
2
+ import getSpaces from './controllers/getSpaces.js';
3
+ import insertSpace from './controllers/insertSpace.js';
4
+ import updateSpace from './controllers/updateSpace.js';
5
+ import deleteSpace from './controllers/deleteSpace.js';
6
+
7
+ const params = {
8
+ config: {
9
+ policy: [
10
+ 'public'
11
+ ]
12
+ }
13
+ };
14
+
15
+ export default async function route(app) {
16
+ app.get('/cms-space/:id?', params, getSpaces);
17
+ app.post('/cms-space/:id?', params, insertSpace);
18
+ app.put('/cms-space/:id?', params, updateSpace);
19
+ app.delete('/cms-space/:id', params, deleteSpace);
20
+ }