@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,162 @@
1
+ import { config, logger, dataInsert, pgClients } from '@opengis/fastify-table/utils.js';
2
+
3
+ import updateCustomContentTable from '../utils/updateCustomContentTable.js'; // custom table
4
+ import updateContents from '../utils/updateContents.js'; // site.contents + site.content_data
5
+
6
+ const columns = [
7
+ {
8
+ name: 'slug',
9
+ label: 'Slug',
10
+ type: 'slug',
11
+ parent: 'title',
12
+ required: true
13
+ },
14
+ {
15
+ name: 'title',
16
+ label: 'Заголовок',
17
+ type: 'text',
18
+ required: true
19
+ },
20
+ {
21
+ name: 'status',
22
+ label: 'Статус',
23
+ type: 'select',
24
+ data: "content.status",
25
+ required: true
26
+ },
27
+ {
28
+ name: 'published_at',
29
+ label: 'Дата публікації',
30
+ type: 'date',
31
+ required: true
32
+ },
33
+ {
34
+ name: 'is_pin',
35
+ label: 'Закріпити контент',
36
+ type: 'boolean',
37
+ required: false,
38
+ default: 'false',
39
+ hidden: true,
40
+ },
41
+ {
42
+ name: 'meta',
43
+ label: 'Meta',
44
+ type: 'datalist',
45
+ hidden: true,
46
+ },
47
+ {
48
+ name: 'created_at',
49
+ label: 'Дата та час створення',
50
+ type: 'datetime',
51
+ default: 'now()',
52
+ hidden: true,
53
+ },
54
+ {
55
+ name: 'updated_at',
56
+ label: 'Дата та час редагування',
57
+ type: 'datetime',
58
+ default: 'now()',
59
+ hidden: true,
60
+ },
61
+ {
62
+ name: 'created_by',
63
+ type: 'text',
64
+ label: 'Автор',
65
+ hidden: true
66
+ },
67
+ {
68
+ name: 'updated_by',
69
+ type: 'text',
70
+ label: 'Редактор',
71
+ default: 'now()',
72
+ hidden: true
73
+ }
74
+ ];
75
+
76
+ export default async function builderPost(req, reply) {
77
+ const { pg = pgClients.client, user = {}, body = {} } = req;
78
+
79
+ if (!pg?.pk?.['site.content_types']) {
80
+ return reply.status(404).send('table not found');
81
+ }
82
+
83
+ const { customTable = true } = body;
84
+
85
+ if (!body.name) {
86
+ return reply.status(400).send('not enough body params: name');
87
+ }
88
+
89
+ if (typeof body.name !== 'string') {
90
+ return reply.status(400).send('invalid body params: name not as string');
91
+ }
92
+
93
+ const rowCount = await pg.query('select content_type_id as id from site.content_types where name=$1', [body.name]).then(el => el.rowCount);
94
+
95
+ if (rowCount > 0) {
96
+ return reply.status(400).send('Тип контенту з даною назвою вже існує');
97
+ }
98
+
99
+ const tablename = body.type === 'collection' && true ? body.name.toLowerCase().replace(/ /g, '').replace(/'/g, "''").replace(/'/g, '').replace(/-/g, '_') : undefined; // customTable
100
+
101
+ const client = await pg.connect();
102
+
103
+ try {
104
+ await client.query('BEGIN');
105
+
106
+ const columns1 = (body.fields || body.columns || columns.filter(el => !el.hidden)).filter(el => el.name).map(el => Object.assign(el, { name: el.name.toLowerCase() })); // for rest crud
107
+
108
+ if (body.type === 'single') {
109
+ const singleColumns = [
110
+ {
111
+ name: 'main_image',
112
+ label: 'Головне зображення',
113
+ type: 'image',
114
+ required: false
115
+ },
116
+ {
117
+ name: 'single_body',
118
+ label: 'Контент',
119
+ type: 'richtext',
120
+ required: false
121
+ },
122
+ {
123
+ name: 'single_sections',
124
+ label: 'Секції',
125
+ type: 'reference',
126
+ required: false
127
+ },
128
+ ]
129
+ columns1.push(...singleColumns);
130
+ }
131
+
132
+ const { rows = [] } = await dataInsert({
133
+ pg: client,
134
+ table: 'site.content_types',
135
+ data: {
136
+ ...body,
137
+ table_name: tablename,
138
+ columns: columns1,
139
+ },
140
+ uid: user?.uid,
141
+ });
142
+
143
+ Object.assign(body, { slug: body.slug || body.name });
144
+
145
+ if (tablename) {
146
+ await updateCustomContentTable({ pg: client, id: rows?.[0]?.content_type_id, columns });
147
+ }
148
+
149
+ const res = await updateContents(client, rows?.[0]?.content_type_id, body, user?.uid);
150
+ Object.assign(rows?.[0], res || {}); // update rows with contents
151
+
152
+ await client.query('COMMIT');
153
+
154
+ return reply.status(200).send({ id: rows?.[0]?.content_type_id, rows });
155
+ } catch (err) {
156
+ await client.query('ROLLBACK');
157
+ logger.file('cms/contentType', { error: err.toString(), stack: err.stack });
158
+ return reply.status(500).send('Помилка при додаванні типу контенту');
159
+ } finally {
160
+ client.release();
161
+ }
162
+ }
@@ -0,0 +1,54 @@
1
+ import { config, pgClients } from '@opengis/fastify-table/utils.js';
2
+
3
+ const maxLimit = 100;
4
+
5
+ export default async function builderList({ pg = pgClients.client, query = {} }, reply) {
6
+ if (!pg) {
7
+ return reply.status(200).send({ total: 0, msg: 'empty pg', rows: [] });
8
+ }
9
+
10
+ const limit = Math.min(maxLimit, query.limit || 16);
11
+ const offset = query.page && query.page > 0 ? (query.page - 1) * limit : 0;
12
+
13
+ const q = `select a.content_type_id as id, a.name, a.title, a.description, a.table_name as table, type, columns, a.include_search, json_build_object('locales', locales, 'default_locale', default_locale, 'name', space_name) as space FROM site.content_types a
14
+ left join lateral ( select locales, default_locale, name as space_name from site.spaces where space_id= 'default' limit 1) b on 1=1
15
+ where ${query.type === 'collection' ? `type='collection'` : (query.type === 'single' ? `type='single'` : '1=1')}`;
16
+
17
+ const where = query.search?.replace ? `(name ilike '%${query.search.replace(/'/, "''")}%' or title ilike '%${query.search.replace(/'/, "''")}%')` : '1=1';
18
+ const q1 = q + `and ${where} order by case when status='archived' then true else false end limit ${limit} offset ${offset}`;
19
+
20
+ if (query.sql) return q1;
21
+
22
+ const { rows = [] } = await pg.query(q1);
23
+
24
+ const { total = 0, filtered = 0 } = await pg.query(`select
25
+ count(*)::int as total,
26
+ count(*) filter (where ${where})::int as filtered
27
+ from (${q})q`
28
+ ).then(el => el.rows?.[0] || {});
29
+
30
+ await Promise.all(rows.map(async row => {
31
+ if (row.table && false) {
32
+ await pg.query('alter table data.' + row.table + ' add column if not exists created_at timestamp with time zone not null default now()');
33
+ await pg.query('alter table data.' + row.table + ' add column if not exists updated_at timestamp with time zone not null default now()');
34
+ await pg.query('alter table data.' + row.table + ' add column if not exists created_by text');
35
+ await pg.query('alter table data.' + row.table + ' add column if not exists updated_by text');
36
+ }
37
+
38
+ const q2 = row.table
39
+ ? `select count(*) from data."${row.table}" where $1=$1`
40
+ : `select count(*) from site.contents where $1=$1 and ${row.id === 'pages' ? `slug <> 'pages' and content_type_id in (select content_type_id from site.content_types where type = 'single' or content_type_id='pages')` : 'content_type_id=$1'}`;
41
+
42
+ const q1 = row.table
43
+ ? `select updated_at from data."${row.table}" where $1=$1 order by updated_at desc limit 1`
44
+ : `select updated_at from site.content_data where content_id in ( select content_id from site.contents where content_type_id=$1 limit 1) order by updated_at desc limit 1`;
45
+
46
+ const count = await pg.queryCache(q2, { table: row.table ? `data.${row.table}` : 'site.content_data', args: [row.id] }).then(el => el.rows?.[0]?.count || '0');
47
+
48
+ const lastEdit = await pg.queryCache(q1, { table: row.table ? `data.${row.table}` : 'site.content_data', args: [row.id] }).then(el => el.rows?.[0]?.updated_at);
49
+
50
+ Object.assign(row, { columns: undefined, fields: row.columns?.length || '0', entries: +count, last_edit: lastEdit });
51
+ }));
52
+
53
+ return reply.status(200).send({ total, filtered, count: rows.length, provider: 'pg', rows });
54
+ }
@@ -0,0 +1,75 @@
1
+ import { dataUpdate, dataDelete, pgClients } from '@opengis/fastify-table/utils.js';
2
+
3
+ export default async function builderDelete({ pg = pgClients.client, user = {}, params = {} }, reply) {
4
+ if (!params.id) {
5
+ return reply.status(400).send('not enough params: id');
6
+ }
7
+
8
+ if (!pg?.pk?.['site.content_types']) {
9
+ return reply.status(404).send('table not found');
10
+ }
11
+
12
+ const { cid, table, title, type } = await pg.query(
13
+ `select content_type_id as cid, table_name as table, title, type from site.content_types where $1 in (content_type_id, name)`,
14
+ [params.id],
15
+ ).then(el => el.rows?.[0] || {});
16
+
17
+ if (!cid) {
18
+ return reply.status(404).send('content type not found');
19
+ }
20
+
21
+ if (cid === 'pages') {
22
+ return reply.status(403).send('access restricted: pages content type cannot be deleted');
23
+ }
24
+
25
+ // await pg.query('delete from site.content_data where content_id in (select content_id from site.contents where content_type_id=$1)', [cid]);
26
+ // await pg.query('delete from site.contents where content_type_id=$1', [cid]);
27
+
28
+ if (table) {
29
+ // await pg.query(`drop table if exists data."${table}"`);
30
+ if (pg?.pk && false) {
31
+ delete pg.pk[`data."${table}"`];
32
+ pg.tlist = pg.tlist.filter(el => el !== `data."${table}"`);
33
+ }
34
+
35
+ // await pg.query('delete from site.content_data where content_id in (select content_id from site.contents where content_type_id=$1)', [cid])
36
+
37
+ // await pg.query('delete from site.contents where content_type_id=$1', [cid]);
38
+ }
39
+
40
+ // cascade
41
+ const contentIds = await pg.query('select array_agg(content_id) from site.contents where content_type_id=$1 and title<>$2', [cid, title])
42
+ .then(el => el.rows?.[0]?.array_agg || []);
43
+
44
+ if (contentIds.length) {
45
+ // await pg.query('delete from site.content_data where content_id=any($1)', [contentIds]);
46
+ }
47
+
48
+ if (type === 'collection' && table) {
49
+ const count = await pg.query(`select count(*)::int from data."${table}"`).then(el => el.rows?.[0]?.count || 0);
50
+ if (count === 0) {
51
+ await pg.query('delete from site.contents where content_type_id=$1', [cid]);
52
+ const res = await dataDelete({
53
+ pg,
54
+ table: 'site.content_types',
55
+ id: cid,
56
+ uid: user?.uid
57
+ });
58
+ // await pg.query(`drop table if exists data."${table}"`);
59
+ return { id: cid, rows: [res].filter(Boolean) };
60
+ }
61
+ }
62
+
63
+ // console.log('archived', cid, contentIds);
64
+ const res = await dataUpdate({
65
+ pg,
66
+ table: 'site.content_types',
67
+ id: cid,
68
+ data: {
69
+ status: 'archived',
70
+ },
71
+ uid: user?.uid
72
+ });
73
+
74
+ return { id: cid, rows: [res].filter(Boolean) };
75
+ }
@@ -0,0 +1,61 @@
1
+ import { config, dataUpdate, pgClients } from '@opengis/fastify-table/utils.js';
2
+
3
+ import updateCustomContentTable from '../utils/updateCustomContentTable.js';
4
+ import updateContents from '../utils/updateContents.js';
5
+
6
+ export default async function builderPut(req, reply) {
7
+ const { pg = pgClients.client, body = {}, params = {}, user = {} } = req;
8
+ if (!params.id) {
9
+ return reply.status(400).send('not enough params: id');
10
+ }
11
+
12
+ if (!pg?.pk?.['site.content_types']) {
13
+ return reply.status(404).send('table not found');
14
+ }
15
+
16
+ const { cid, table } = await pg.query(
17
+ `select content_type_id as cid, table_name as table, type from site.content_types where $1 in (content_type_id, name)`,
18
+ [params.id],
19
+ ).then(el => el.rows?.[0] || {});
20
+
21
+ if (!cid) {
22
+ return reply.status(404).send('content type not found');
23
+ }
24
+
25
+ const columns = (body.fields || body.columns)?.filter?.(el => el.name)?.map?.(el => Object.assign(el, { name: el.name.toLowerCase() })); // for rest crud
26
+
27
+ const client = await pg.connect();
28
+
29
+ client.options = pg.options;
30
+ client.tlist = pg.tlist;
31
+ client.pgType = pg.pgType;
32
+ client.relkinds = pg.relkinds;
33
+ client.pk = pg.pk;
34
+
35
+ try {
36
+ await client.query('begin');
37
+
38
+ if (table) {
39
+ await updateCustomContentTable({ pg: client, id: cid, columns });
40
+ }
41
+
42
+ await dataUpdate({
43
+ pg: client,
44
+ table: 'site.content_types',
45
+ id: cid,
46
+ data: body,
47
+ uid: user?.uid,
48
+ });
49
+
50
+ const row = await updateContents(client, cid, { ...body, slug: body.slug || body.name }, user?.uid);
51
+
52
+ await client.query('commit');
53
+
54
+ return { id: row?.content_type_id, rows: [row].filter(Boolean) };
55
+ } catch (err) {
56
+ await client.query('rollback');
57
+ throw err;
58
+ } finally {
59
+ client.release();
60
+ }
61
+ }
@@ -0,0 +1,37 @@
1
+ import { pgClients, getTemplate } from '@opengis/fastify-table/utils.js';
2
+
3
+ export default async function builderGet({ pg = pgClients.client, params = {} }, reply) {
4
+ if (!params.id) {
5
+ return reply.status(400).send('not enough params: id');
6
+ }
7
+
8
+ if (!pg?.pk?.['site.content_types']) {
9
+ return reply.status(404).send('table not found');
10
+ }
11
+
12
+ const row = await pg.query(
13
+ `select content_type_id as id, * FROM site.content_types
14
+ where $1 in (content_type_id, name)`, [params.id]
15
+ ).then(el => el.rows?.[0]);
16
+
17
+ if (!row) {
18
+ return reply.status(404).send('content type not found');
19
+ }
20
+
21
+ const space = await pg.query(
22
+ 'select name, default_locale, locales from site.spaces where space_id = $1', ['default'],
23
+ ).then(el => el.rows?.[0] || {});
24
+ Object.assign(row, { space });
25
+
26
+ const loadTable = params.id === 'pages' ? await getTemplate('table', 'single.default.table') : {};
27
+
28
+ const columns = params.id === 'pages' ? loadTable?.columns : row.columns;
29
+ Object.assign(row, { columns });
30
+
31
+ // filter system / hidden
32
+ if (Array.isArray(row.columns)) {
33
+ row.columns.filter(col => !col.hidden);
34
+ }
35
+
36
+ return reply.status(200).send(row);
37
+ }
@@ -1,25 +1,35 @@
1
- import cmsTypeGet from './controllers/cms.type.get.js';
2
- import cmsTypeList from './controllers/cms.type.list.js';
3
- import cmsTypePut from './controllers/cms.type.put.js';
4
- import cmsTypePost from './controllers/cms.type.post.js';
5
- import cmsTypeDelete from './controllers/cms.type.delete.js';
1
+ import getContentType from './controllers/getContentType.js';
2
+ import contentTypeList from './controllers/contentTypeList.js';
3
+ import editContentType from './controllers/editContentType.js';
4
+ import addContentType from './controllers/addContentType.js';
5
+ import delContentType from './controllers/delContentType.js';
6
6
 
7
- const policy = ['public'];
7
+ const params = { config: { policy: ['public'] } };
8
8
 
9
9
  export default async function route(app) {
10
- // Type add
11
- app.post('/cms-type', { config: { policy } }, cmsTypePost);
10
+ const schema = {
11
+ params: {
12
+ type: 'object',
13
+ properties: {
14
+ id: {
15
+ type: 'string'
16
+ },
17
+ },
18
+ },
19
+ body: {
20
+ type: 'object',
21
+ required: ['name'],
22
+ properties: {
23
+ name: { type: 'string' }
24
+ }
25
+ }
26
+ };
12
27
 
13
- // Type edit
14
- app.put('/cms-type/:id', { config: { policy } }, cmsTypePut);
28
+ app.get('/cms', params, contentTypeList);
29
+ app.get('/cms-type', params, contentTypeList);
15
30
 
16
- // Type list
17
- app.get('/cms', { config: { policy } }, cmsTypeList);
18
- app.get('/cms-type', { config: { policy } }, cmsTypeList);
19
-
20
- // Type get
21
- app.get('/cms-type/:id', { config: { policy } }, cmsTypeGet);
22
-
23
- // Type delete
24
- app.delete('/cms-type/:id', { config: { policy } }, cmsTypeDelete);
31
+ app.post('/cms-type', { ...params, schema }, addContentType);
32
+ app.put('/cms-type/:id', { ...params, schema }, editContentType);
33
+ app.get('/cms-type/:id', params, getContentType);
34
+ app.delete('/cms-type/:id', params, delContentType);
25
35
  }
@@ -0,0 +1,29 @@
1
+ import { dataInsert, dataUpdate } from "@opengis/fastify-table/utils.js";
2
+
3
+ export default async function updateContents(pg, id, data, uid) {
4
+ if (!pg?.pk || !id) return null;
5
+
6
+ const contentId = await pg.query(
7
+ `select content_id from site.contents where content_type_id = $1 order by content_id=content_type_id desc limit 1`, // update pages collection, avoid singles
8
+ [id],
9
+ ).then(el => el.rows?.[0]?.content_id);
10
+
11
+ if (contentId) {
12
+ const result = await dataUpdate({
13
+ pg,
14
+ id: contentId,
15
+ table: 'site.contents',
16
+ data,
17
+ uid,
18
+ });
19
+ return result;
20
+ }
21
+
22
+ const result = await dataInsert({
23
+ pg,
24
+ table: 'site.contents',
25
+ data: { ...data || {}, content_type_id: id },
26
+ uid,
27
+ }).then(el => el.rows?.[0]);
28
+ return result;
29
+ }
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+
3
+ import { getMeta } from '@opengis/fastify-table/utils.js';
4
+
5
+ const inputTypes = existsSync('input-types.json') ? JSON.parse(readFileSync('input-types.json') || '{}') : {};
6
+
7
+ const types = {
8
+ number: 'numeric',
9
+ datetime: 'timestamp without time zone',
10
+ boolean: 'boolean',
11
+ date: 'date',
12
+ array: 'text[]',
13
+ FileList: 'text[]',
14
+ };
15
+
16
+ export default async function updateCustomContentTable({ pg, id, columns = [] }) {
17
+ if (!pg?.pk || !id) return null;
18
+
19
+ const { table_name: tablename, dbcolumns } = await pg.query(
20
+ `select table_name, columns as dbcolumns from site.content_types where content_type_id = $1`,
21
+ [id],
22
+ ).then(el => el.rows?.[0] || {});
23
+
24
+ if (!tablename) return null;
25
+
26
+ const droppedColumns = (dbcolumns || []).filter(el => columns.findIndex(col => col.name === el.name) === -1);
27
+ // console.log(droppedColumns);
28
+
29
+ const q = `create table if not exists data."${tablename}"
30
+ ( id text not null default next_id() PRIMARY KEY );
31
+ ${droppedColumns.map(col => `
32
+ alter table data."${tablename}" add column if not exists "${col.name}" text;
33
+ alter table data."${tablename}" alter column "${col.name}" drop not null;`).join('\n')}
34
+ ${columns?.filter?.(col => col.name)?.map?.(col => `
35
+ alter table data."${tablename}" add column if not exists "${col.name}" ${types[col.type] || inputTypes[col.type] || 'text'} ${col.required ? ' not null ' : ''};
36
+ alter table data."${tablename}" alter column "${col.name}" ${col.required ? ' set ' : ' drop '} not null;
37
+ alter table data."${tablename}" alter column "${col.name}" set default ${col.default ? col.default : 'null'};
38
+ comment on column data."${tablename}"."${col.name}" is ${col.label ? `'${col.label.replace(/'/g, "''")}'` : 'null'} `
39
+ ).join(';') || ''}`;
40
+ await pg.query(q);
41
+ await pg.query(`alter table data."${tablename}" add column if not exists created_at timestamp with time zone not null default now()`);
42
+ await pg.query(`alter table data."${tablename}" add column if not exists updated_at timestamp with time zone not null default now()`);
43
+ await pg.query(`alter table data."${tablename}" add column if not exists created_by text`);
44
+ await pg.query(`alter table data."${tablename}" add column if not exists updated_by text`);
45
+ await pg.query(`alter table data."${tablename}" drop constraint if exists c${id}_slug_unique;alter table data."${tablename}" add constraint c${id}_slug_unique UNIQUE (slug);`);
46
+
47
+ // force meta columns refresh to avoid crud skips
48
+ getMeta({ pg, table: `data.${tablename}` }, true);
49
+
50
+ if (pg?.pk && pg.tlist && !pg.tlist.find(el => el === `data.${tablename}`)) {
51
+ pg.pk[`data.${tablename}`] = 'id';
52
+ pg.tlist?.push?.(`data.${tablename}`);
53
+ }
54
+
55
+ return null;
56
+ }
@@ -0,0 +1,25 @@
1
+ export default async function feedbackList(req, reply) {
2
+ const { pg, user = {}, query } = req;
3
+ const { uid } = user;
4
+ if (!uid) return reply.code(401).send({ message: 'Not authorized', status: 401 });
5
+
6
+ const { page = 1, limit = 16 } = query;
7
+ const offset = limit * (page - 1) || 0;
8
+
9
+ const { total, filtered } = await pg.one(`select count(*)::int as total from site.feedback`)
10
+
11
+ const { rows = [] } = await pg.query(`
12
+ select
13
+ row_number() over(order by created_at desc) as num,
14
+ subscription_id,
15
+ email,
16
+ created_at,
17
+ enabled
18
+ from site.subscription
19
+ order by created_at desc
20
+ limit ${limit} offset ${offset}
21
+ `);
22
+
23
+ const status = rows?.length ? 200 : 204;
24
+ return reply.code(status).send({ message: { total, filtered, rows }, status })
25
+ }
@@ -0,0 +1,49 @@
1
+ import { pgClients, dataInsert, config } from '@opengis/fastify-table/utils.js';
2
+
3
+ const refererAllow = config?.refererAllow || ['127.0.0.1', 'localhost', 'softpro.ua', 'opengis.info'];
4
+
5
+ function checkReferer(headers) {
6
+ if (config?.local) return true;
7
+ if (!headers?.referer) return false;
8
+ return refererAllow.some(allowed => headers.referer.includes(allowed));
9
+ }
10
+
11
+ export default async function feedback({ body = {}, pg = pgClients.client, headers, params }, reply) {
12
+ reply
13
+ .header('Access-Control-Allow-Origin', '*')
14
+ .header('Access-Control-Allow-Methods', 'POST, OPTIONS')
15
+ .header('Access-Control-Allow-Headers', 'Content-Type');
16
+
17
+ if (!checkReferer(headers)) {
18
+ return reply.status(403).send('access restricted');
19
+ }
20
+
21
+ if ((params?.type !== 'vacancy') && (!body?.message_text || !body?.email)) {
22
+ return reply.status(400).send('message_text and email is required');
23
+ }
24
+
25
+ if ((params?.type === 'vacancy') && !body?.user_name) {
26
+ return reply.status(400).send('user_name is required');
27
+ }
28
+
29
+ const allowedFields = ['title', 'message_text', 'email', 'phone', 'user_name', 'resume_link', 'linkedin_link', 'portfolio_link', 'telegram_link'];
30
+ const data = Object.fromEntries(
31
+ Object.entries(body).filter(([key]) => allowedFields.includes(key))
32
+ );
33
+ data["feedback_type"] = params?.type;
34
+
35
+ const { rows } = await dataInsert({
36
+ table: 'site.feedback',
37
+ data: {
38
+ ...data,
39
+ headers
40
+ },
41
+ pg
42
+ });
43
+
44
+ if (!rows?.length) {
45
+ return reply.status(500).send('insert error');
46
+ }
47
+
48
+ return reply.status(200).send('Звернення додано успішно');
49
+ }
@@ -0,0 +1,38 @@
1
+ import { getFilterSQL } from '@opengis/fastify-table/utils.js';
2
+
3
+ export default async function feedbackList(req, reply) {
4
+ const { pg, user = {}, query } = req;
5
+ const { uid } = user;
6
+ if (!uid) return reply.code(401).send({ message: 'Not authorized', status: 401 });
7
+
8
+ const { page = 1, limit = 16, filter = null } = query;
9
+ const offset = limit * (page - 1) || 0;
10
+
11
+ const { q: filterQuery } = await getFilterSQL({
12
+ filter, table: 'site.feedback'
13
+ });
14
+
15
+ const { total, filtered } = await pg.one(`select count(*)::int as total, (count(*) filter(where ${filterQuery ? `${filterQuery}` : '1=1'}))::int as filtered from site.feedback`)
16
+
17
+ const { rows = [] } = await pg.query(`
18
+ select
19
+ row_number() over(order by created_at desc) as num,
20
+ email,
21
+ message_id,
22
+ user_name,
23
+ message_text,
24
+ feedback_type,
25
+ phone,
26
+ created_at,
27
+ linkedin_link,
28
+ portfolio_link,
29
+ telegram_link
30
+ from site.feedback
31
+ where created_at is not null ${filterQuery ? `and ${filterQuery}` : ''}
32
+ order by created_at desc
33
+ limit ${limit} offset ${offset}
34
+ `);
35
+
36
+ const status = rows?.length ? 200 : 204;
37
+ return reply.code(status).send({ message: { total, filtered, rows }, status })
38
+ }