@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
@@ -2,71 +2,25 @@ create schema if not exists admin;
2
2
  create table if not exists admin.users (uid text primary key default next_id());
3
3
 
4
4
  create schema if not exists site;
5
+ create schema if not exists data;
5
6
 
6
7
  -- drop table if exists site.categories cascade;
7
8
  create table if not exists site.categories (
8
9
  category_id text primary key default next_id(),
10
+ name text not null,
9
11
  created_at TIMESTAMP DEFAULT NOW(),
10
12
  updated_at TIMESTAMP DEFAULT NOW(),
11
13
  created_by text REFERENCES admin.users(uid),
12
14
  updated_by text REFERENCES admin.users(uid)
13
15
  );
14
16
 
15
- -- drop table if exists site.articles cascade;
16
- CREATE TABLE if not exists site.articles (
17
- articles_id text PRIMARY KEY default next_id(),
18
- title VARCHAR (100) NOT NULL,
19
- slug VARCHAR (50) UNIQUE,
20
- content TEXT,
21
- excerpt TEXT,
22
- status VARCHAR not null DEFAULT 'draft' CHECK (status::text = ANY (ARRAY['draft', 'published', 'archived']::text[])),
23
- published_at TIMESTAMP,
24
- is_visible BOOLEAN not null DEFAULT TRUE,
25
- meta_title VARCHAR,
26
- meta_description TEXT,
27
- created_at TIMESTAMP DEFAULT NOW(),
28
- updated_at TIMESTAMP DEFAULT NOW(),
29
- created_by text REFERENCES admin.users(uid),
30
- updated_by text REFERENCES admin.users(uid),
31
- author_id text REFERENCES admin.users(uid) ON DELETE SET NULL,
32
- category_id text REFERENCES site.categories(category_id) ON DELETE SET NULL
33
- );
34
-
35
- alter table site.articles add column if not exists category_id text REFERENCES site.categories(category_id) ON DELETE SET NULL;
36
- alter table site.articles add column if not exists meta_title VARCHAR;
37
- alter table site.articles add column if not exists meta_description TEXT;
38
-
39
- CREATE INDEX if not exists idx_published_articles ON site.articles(published_at) WHERE status = 'published';
40
- CREATE INDEX if not exists idx_articles_status ON site.articles(status);
41
- CREATE INDEX if not exists idx_articles_category_id ON site.articles(category_id);
42
- CREATE INDEX if not exists idx_articles_author_id ON site.articles(author_id);
43
- CREATE INDEX if not exists idx_articles_is_visible ON site.articles(is_visible);
44
- CREATE INDEX if not exists idx_articles_published_at ON site.articles(published_at);
45
-
46
- COMMENT ON TABLE site.articles IS 'Articles for the site, including content, metadata, status, and authorship.';
47
- COMMENT ON COLUMN site.articles.articles_id IS 'Primary key for the article, generated via next_id().';
48
- COMMENT ON COLUMN site.articles.title IS 'Title of the article.';
49
- COMMENT ON COLUMN site.articles.slug IS 'URL-friendly unique identifier for the article.';
50
- COMMENT ON COLUMN site.articles.content IS 'Full body content of the article.';
51
- COMMENT ON COLUMN site.articles.excerpt IS 'Optional summary or teaser text.';
52
- COMMENT ON COLUMN site.articles.status IS 'Publication status: draft, published, or archived.';
53
- COMMENT ON COLUMN site.articles.published_at IS 'Timestamp when the article was published.';
54
- COMMENT ON COLUMN site.articles.is_visible IS 'Boolean flag indicating if the article is visible on the site.';
55
- COMMENT ON COLUMN site.articles.meta_title IS 'SEO meta title for the article.';
56
- COMMENT ON COLUMN site.articles.meta_description IS 'SEO meta description for the article.';
57
- COMMENT ON COLUMN site.articles.created_at IS 'Timestamp when the article was created.';
58
- COMMENT ON COLUMN site.articles.updated_at IS 'Timestamp when the article was last updated.';
59
- COMMENT ON COLUMN site.articles.created_by IS 'User ID of the admin who created the article.';
60
- COMMENT ON COLUMN site.articles.updated_by IS 'User ID of the admin who last updated the article.';
61
- COMMENT ON COLUMN site.articles.author_id IS 'User ID of the article''s author.';
62
- COMMENT ON COLUMN site.articles.category_id IS 'Category ID to which the article belongs.';
63
-
64
17
  -- drop table if exists site.media cascade;
65
18
  CREATE TABLE if not exists site.media (
66
19
  media_id text PRIMARY KEY default next_id(),
67
20
  filename TEXT NOT NULL,
68
21
  filetype TEXT NOT NULL,
69
22
  filesize INTEGER NOT NULL,
23
+ subdir TEXT,
70
24
  url TEXT,
71
25
  description TEXT,
72
26
  alt text,
@@ -83,6 +37,7 @@ COMMENT ON COLUMN site.media.media_id IS 'Unique ID for the media item.';
83
37
  COMMENT ON COLUMN site.media.filename IS 'Original name of the uploaded file.';
84
38
  COMMENT ON COLUMN site.media.filetype IS 'Logical type of file (e.g., image, video, document).';
85
39
  COMMENT ON COLUMN site.media.filesize IS 'Size of the file in bytes.';
40
+ -- COMMENT ON COLUMN site.media.subdir is 'Піддиректорія';
86
41
  COMMENT ON COLUMN site.media.url IS 'URL where the file can be accessed.';
87
42
  COMMENT ON COLUMN site.media.description IS 'Optional description or caption.';
88
43
  COMMENT ON COLUMN site.media.alt IS 'Alternative text for screen readers or image fallbacks.';
@@ -101,38 +56,11 @@ CREATE INDEX if not exists idx_media_created_by ON site.media(created_by);
101
56
  CREATE INDEX if not exists idx_media_updated_at ON site.media(updated_at);
102
57
  CREATE INDEX if not exists idx_media_updated_by ON site.media(updated_by);
103
58
 
104
- -- drop table if exists site.article_media cascade;
105
- CREATE TABLE if not exists site.article_media (
106
- article_id text not null REFERENCES site.articles(articles_id) ON DELETE CASCADE,
107
- media_id text not null REFERENCES site.media(media_id) ON DELETE CASCADE,
108
- related_id text NOT NULL,
109
- related_type VARCHAR(50) NOT NULL,
110
- field VARCHAR(50) NOT NULL,
111
- order_index INTEGER DEFAULT 0,
112
- created_at TIMESTAMP DEFAULT NOW(),
113
- updated_at TIMESTAMP DEFAULT NOW(),
114
- created_by text REFERENCES admin.users(uid),
115
- updated_by text REFERENCES admin.users(uid),
116
- PRIMARY KEY (article_id, media_id, field)
117
- );
118
-
119
- COMMENT ON TABLE site.article_media IS 'Join table for associating media with articles, supporting multiple fields and order.';
120
- COMMENT ON COLUMN site.article_media.article_id IS 'Foreign key to the article that uses the media.';
121
- COMMENT ON COLUMN site.article_media.media_id IS 'Foreign key to the media file linked to the article.';
122
- COMMENT ON COLUMN site.article_media.related_id IS 'ID of the related entity (for polymorphic associations).';
123
- COMMENT ON COLUMN site.article_media.related_type IS 'Type of the related entity (e.g., article, gallery).';
124
- COMMENT ON COLUMN site.article_media.field IS 'Field name that defines the role of media (e.g., cover, inline).';
125
- COMMENT ON COLUMN site.article_media.order_index IS 'Ordering index for multiple media in the same field.';
126
-
127
- CREATE INDEX if not exists idx_article_media_related ON site.article_media(related_type, related_id);
128
- CREATE INDEX if not exists idx_article_media_ordering ON site.article_media(article_id, field, order_index);
129
-
130
59
  -- drop table if exists site.content_types cascade;
131
60
  CREATE TABLE if not exists site.content_types (
132
61
  content_type_id text PRIMARY KEY default next_id(),
133
62
  name VARCHAR (50) NOT NULL UNIQUE,
134
63
  title VARCHAR (50) NOT NULL,
135
- slug VARCHAR (50) NOT NULL UNIQUE,
136
64
  table_name VARCHAR(50),
137
65
  columns json,
138
66
  status VARCHAR (20) DEFAULT 'draft' CHECK (status::text = ANY (ARRAY['draft', 'published', 'archived']::text[])),
@@ -143,16 +71,21 @@ CREATE TABLE if not exists site.content_types (
143
71
  description TEXT,
144
72
  icon VARCHAR (50),
145
73
  color VARCHAR (20),
74
+ preview_path text,
146
75
  created_at TIMESTAMP DEFAULT NOW(),
147
76
  updated_at TIMESTAMP DEFAULT NOW(),
148
77
  created_by text REFERENCES admin.users(uid),
149
78
  updated_by text REFERENCES admin.users(uid)
150
79
  );
151
80
 
81
+ ALTER TABLE if exists site.content_types ALTER COLUMN table_name DROP NOT NULL;
82
+ ALTER TABLE if exists site.content_types ADD COLUMN if not exists columns json;
83
+
152
84
  COMMENT ON TABLE site.content_types IS 'Defines reusable content structures for dynamic content management.';
153
85
  COMMENT ON COLUMN site.content_types.content_type_id IS 'Unique identifier for the content type.';
154
86
  COMMENT ON COLUMN site.content_types.name IS 'Internal name used in the system (must be unique).';
155
- COMMENT ON COLUMN site.content_types.name IS 'Title';
87
+ COMMENT ON COLUMN site.content_types.name IS 'Name / Alias / slug';
88
+ COMMENT ON COLUMN site.content_types.title IS 'Title';
156
89
  COMMENT ON COLUMN site.content_types.table_name IS 'Physical table where entries of this type are stored.';
157
90
  COMMENT ON COLUMN site.content_types.status IS 'Status of the content type (draft, published, archived).';
158
91
  COMMENT ON COLUMN site.content_types.visible IS 'Controls whether this content type appears in admin UI.';
@@ -166,124 +99,15 @@ COMMENT ON COLUMN site.content_types.created_at IS 'Timestamp when the content t
166
99
  COMMENT ON COLUMN site.content_types.updated_at IS 'Timestamp when the content type was last modified.';
167
100
  COMMENT ON COLUMN site.content_types.created_by IS 'Admin user who created the content type.';
168
101
  COMMENT ON COLUMN site.content_types.updated_by IS 'Admin user who last updated the content type.';
102
+ COMMENT ON COLUMN site.content_types.preview_path IS 'Назва інтерфейсу на сайті';
103
+
104
+ ALTER TABLE if exists site.content_types ADD COLUMN if not exists include_search boolean;
169
105
 
170
106
  CREATE INDEX if not exists idx_content_types_status ON site.content_types(status);
171
107
  CREATE INDEX if not exists idx_content_types_visible ON site.content_types(visible);
172
108
  CREATE INDEX if not exists idx_content_types_type ON site.content_types(type);
173
109
  CREATE INDEX if not exists idx_content_types_table_name ON site.content_types(table_name);
174
110
 
175
- -- drop table if exists site.content_attributes cascade;
176
- CREATE TABLE if not exists site.content_attributes (
177
- content_attribute_id text PRIMARY KEY default next_id(),
178
- content_type_id text REFERENCES site.content_types(content_type_id) ON DELETE CASCADE,
179
- name VARCHAR NOT NULL,
180
- type VARCHAR NOT NULL,
181
- is_required BOOLEAN DEFAULT FALSE,
182
- is_unique BOOLEAN DEFAULT FALSE,
183
- minLength numeric,
184
- maxLength numeric,
185
- private BOOLEAN DEFAULT FALSE,
186
- min numeric,
187
- max numeric,
188
- relation_type VARCHAR (20),
189
- related_to VARCHAR(50),
190
- default_value TEXT,
191
- options JSONB,
192
- enum text[],
193
- created_at TIMESTAMP DEFAULT NOW(),
194
- updated_at TIMESTAMP DEFAULT NOW(),
195
- created_by text REFERENCES admin.users(uid),
196
- updated_by text REFERENCES admin.users(uid)
197
- );
198
-
199
- COMMENT ON TABLE site.content_attributes IS 'Defines fields (attributes) for a dynamic content type schema.';
200
- COMMENT ON COLUMN site.content_attributes.content_attribute_id IS 'Primary key for the content attribute.';
201
- COMMENT ON COLUMN site.content_attributes.content_type_id IS 'Foreign key to the content type this attribute belongs to.';
202
- COMMENT ON COLUMN site.content_attributes.name IS 'Machine name of the attribute.';
203
- COMMENT ON COLUMN site.content_attributes.type IS 'Data type of the attribute (e.g., text, number, boolean).';
204
- COMMENT ON COLUMN site.content_attributes.is_required IS 'Whether this field must be filled.';
205
- COMMENT ON COLUMN site.content_attributes.is_unique IS 'Whether values for this field must be unique.';
206
- COMMENT ON COLUMN site.content_attributes.minLength IS 'Minimum length constraint (for strings).';
207
- COMMENT ON COLUMN site.content_attributes.maxLength IS 'Maximum length constraint (for strings).';
208
- COMMENT ON COLUMN site.content_attributes.private IS 'Whether this field is hidden in public APIs.';
209
- COMMENT ON COLUMN site.content_attributes.min IS 'Minimum numeric value (if applicable).';
210
- COMMENT ON COLUMN site.content_attributes.max IS 'Maximum numeric value (if applicable).';
211
- COMMENT ON COLUMN site.content_attributes.relation_type IS 'Type of relation, if any (e.g., one-to-many).';
212
- COMMENT ON COLUMN site.content_attributes.related_to IS 'Target entity/content type this field relates to.';
213
- COMMENT ON COLUMN site.content_attributes.default_value IS 'Default value assigned to this field.';
214
- COMMENT ON COLUMN site.content_attributes.options IS 'Custom configuration or metadata in JSON format.';
215
- COMMENT ON COLUMN site.content_attributes.enum IS 'List of allowed enum values for this field.';
216
- COMMENT ON COLUMN site.content_attributes.created_at IS 'Timestamp when this attribute was created.';
217
- COMMENT ON COLUMN site.content_attributes.updated_at IS 'Timestamp when this attribute was last updated.';
218
- COMMENT ON COLUMN site.content_attributes.created_by IS 'Admin user who created the content type.';
219
- COMMENT ON COLUMN site.content_attributes.updated_by IS 'Admin user who last updated the content type.';
220
-
221
- CREATE INDEX if not exists idx_attributes_by_type ON site.content_attributes(content_type_id);
222
- CREATE INDEX if not exists idx_attributes_name ON site.content_attributes(name);
223
- CREATE INDEX if not exists idx_attributes_type ON site.content_attributes(type);
224
-
225
- -- drop table if exists site.article_translations cascade;
226
- CREATE TABLE if not exists site.article_translations (
227
- article_translation_id text PRIMARY KEY default next_id(),
228
- article_id text REFERENCES site.articles(articles_id) ON DELETE CASCADE,
229
- locale VARCHAR(10) NOT NULL,
230
- title VARCHAR (100) NOT NULL,
231
- content TEXT,
232
- excerpt TEXT,
233
- meta_title VARCHAR (100),
234
- meta_description TEXT,
235
- created_at TIMESTAMP DEFAULT NOW(),
236
- updated_at TIMESTAMP DEFAULT NOW(),
237
- created_by text REFERENCES admin.users(uid),
238
- updated_by text REFERENCES admin.users(uid),
239
- UNIQUE (article_id, locale)
240
- );
241
-
242
- COMMENT ON TABLE site.article_translations IS 'Stores translated content for articles across different locales.';
243
- COMMENT ON COLUMN site.article_translations.article_translation_id IS 'Primary key for the article translation.';
244
- COMMENT ON COLUMN site.article_translations.article_id IS 'Foreign key linking to the main article.';
245
- COMMENT ON COLUMN site.article_translations.locale IS 'Locale code for the translation (e.g., en, fr).';
246
- COMMENT ON COLUMN site.article_translations.title IS 'Translated title of the article.';
247
- COMMENT ON COLUMN site.article_translations.content IS 'Translated content body.';
248
- COMMENT ON COLUMN site.article_translations.excerpt IS 'Translated excerpt or teaser text.';
249
- COMMENT ON COLUMN site.article_translations.meta_title IS 'Translated SEO meta title.';
250
- COMMENT ON COLUMN site.article_translations.meta_description IS 'Translated SEO meta description.';
251
- COMMENT ON COLUMN site.article_translations.created_at IS 'Timestamp of when this translation was created.';
252
- COMMENT ON COLUMN site.article_translations.updated_at IS 'Timestamp of when this translation was last updated.';
253
- COMMENT ON COLUMN site.article_translations.created_by IS 'Admin user who created this translation.';
254
- COMMENT ON COLUMN site.article_translations.updated_by IS 'Admin user who last updated this translation.';
255
-
256
- CREATE INDEX if not exists idx_article_translations_article_id ON site.article_translations(article_id);
257
- CREATE INDEX if not exists idx_article_translations_locale ON site.article_translations(locale);
258
-
259
- -- drop table if exists site.single_type_values cascade;
260
- CREATE TABLE if not exists site.single_type_values (
261
- single_type_value_id text PRIMARY KEY default next_id(),
262
- type_name VARCHAR(100) NOT NULL,
263
- key VARCHAR(255) NOT NULL,
264
- value TEXT,
265
- value_type VARCHAR(50),
266
- created_at TIMESTAMP DEFAULT NOW(),
267
- updated_at TIMESTAMP DEFAULT NOW(),
268
- created_by text REFERENCES admin.users(uid),
269
- updated_by text REFERENCES admin.users(uid),
270
- UNIQUE(type_name, key)
271
- );
272
-
273
- COMMENT ON TABLE site.single_type_values IS 'Stores key-value pairs for single-type settings or configurations.';
274
- COMMENT ON COLUMN site.single_type_values.single_type_value_id IS 'Primary key for the single type value entry.';
275
- COMMENT ON COLUMN site.single_type_values.type_name IS 'Type of the setting or configuration.';
276
- COMMENT ON COLUMN site.single_type_values.key IS 'Unique key for the setting or configuration value.';
277
- COMMENT ON COLUMN site.single_type_values.value IS 'The value associated with the key.';
278
- COMMENT ON COLUMN site.single_type_values.value_type IS 'Type of value (e.g., string, integer, boolean).';
279
- COMMENT ON COLUMN site.single_type_values.created_at IS 'Timestamp when the value entry was created.';
280
- COMMENT ON COLUMN site.single_type_values.updated_at IS 'Timestamp when the value entry was last updated.';
281
- COMMENT ON COLUMN site.single_type_values.created_by IS 'User who created the value entry.';
282
- COMMENT ON COLUMN site.single_type_values.updated_by IS 'User who last updated the value entry.';
283
-
284
- CREATE INDEX if not exists idx_single_type_values_type_name ON site.single_type_values(type_name);
285
- CREATE INDEX if not exists idx_single_type_values_key ON site.single_type_values(key);
286
-
287
111
  -- drop table if exists site.settings cascade;
288
112
  CREATE TABLE if not exists site.settings (
289
113
  id serial PRIMARY KEY CHECK (id = 1),
@@ -309,8 +133,10 @@ CREATE INDEX if not exists idx_settings_key ON site.settings(key);
309
133
  -- drop table if exists site.menus cascade;
310
134
  CREATE TABLE if not exists site.menus (
311
135
  menu_id text PRIMARY KEY default next_id(),
312
- name VARCHAR(100) UNIQUE NOT NULL,
313
- slug VARCHAR(100) UNIQUE NOT NULL,
136
+ name VARCHAR(100) NOT NULL,
137
+ locale VARCHAR(20) NOT NULL,
138
+ content TEXT,
139
+ items json,
314
140
  description TEXT,
315
141
  created_at TIMESTAMP DEFAULT NOW(),
316
142
  updated_at TIMESTAMP DEFAULT NOW(),
@@ -318,10 +144,17 @@ CREATE TABLE if not exists site.menus (
318
144
  updated_by text REFERENCES admin.users(uid)
319
145
  );
320
146
 
147
+ alter table site.menus add column if not exists locale VARCHAR(20) not null default 'uk';
148
+ ALTER TABLE site.menus DROP CONSTRAINT if exists menus_name_key;
149
+ ALTER TABLE site.menus DROP CONSTRAINT if exists menus_name_locale_unique;
150
+ alter table site.menus drop column if exists lang;
151
+
321
152
  COMMENT ON TABLE site.menus IS 'Stores menu information with a name, slug, description, and audit fields.';
322
153
  COMMENT ON COLUMN site.menus.menu_id IS 'Primary key for the menu (auto-generated).';
323
154
  COMMENT ON COLUMN site.menus.name IS 'Unique name for the menu.';
324
- COMMENT ON COLUMN site.menus.slug IS 'Unique slug for the menu, used for URLs or other references.';
155
+ COMMENT ON COLUMN site.menus.locale IS 'Localization';
156
+ COMMENT ON COLUMN site.menus.content IS 'Menu items in yml format.';
157
+ COMMENT ON COLUMN site.menus.items IS 'Menu items in json format.';
325
158
  COMMENT ON COLUMN site.menus.description IS 'Optional description of the menu.';
326
159
  COMMENT ON COLUMN site.menus.created_at IS 'Timestamp when the menu was created.';
327
160
  COMMENT ON COLUMN site.menus.updated_at IS 'Timestamp when the menu was last updated.';
@@ -329,41 +162,7 @@ COMMENT ON COLUMN site.menus.created_by IS 'Admin user who created the menu.';
329
162
  COMMENT ON COLUMN site.menus.updated_by IS 'Admin user who last updated the menu.';
330
163
 
331
164
  CREATE INDEX if not exists idx_menus_name ON site.menus(name);
332
- CREATE INDEX if not exists idx_menus_slug ON site.menus(slug);
333
-
334
- -- drop table if exists site.menu_items cascade;
335
- CREATE TABLE if not exists site.menu_items (
336
- menu_item_id text PRIMARY KEY default next_id(),
337
- menu_id text NOT NULL REFERENCES site.menus(menu_id) ON DELETE CASCADE,
338
- parent_id text REFERENCES site.menu_items(menu_item_id) ON DELETE CASCADE,
339
- title VARCHAR(255) NOT NULL,
340
- url TEXT,
341
- order_index INTEGER DEFAULT 0,
342
- target VARCHAR(20),
343
- icon VARCHAR(100),
344
- created_at TIMESTAMP DEFAULT NOW(),
345
- updated_at TIMESTAMP DEFAULT NOW(),
346
- created_by text REFERENCES admin.users(uid),
347
- updated_by text REFERENCES admin.users(uid)
348
- );
349
-
350
- COMMENT ON TABLE site.menu_items IS 'Stores items within menus, supporting hierarchical (nested) structures.';
351
- COMMENT ON COLUMN site.menu_items.menu_item_id IS 'Primary key for the menu item (auto-generated).';
352
- COMMENT ON COLUMN site.menu_items.menu_id IS 'Foreign key linking to the associated menu.';
353
- COMMENT ON COLUMN site.menu_items.parent_id IS 'Foreign key linking to the parent menu item (for nested structure).';
354
- COMMENT ON COLUMN site.menu_items.title IS 'Title of the menu item.';
355
- COMMENT ON COLUMN site.menu_items.url IS 'URL or path for the menu item.';
356
- COMMENT ON COLUMN site.menu_items.order_index IS 'Index used for ordering the menu items.';
357
- COMMENT ON COLUMN site.menu_items.target IS 'Target attribute for the menu link (e.g., "_blank" for a new tab).';
358
- COMMENT ON COLUMN site.menu_items.icon IS 'Icon associated with the menu item.';
359
- COMMENT ON COLUMN site.menu_items.created_at IS 'Timestamp when the menu item was created.';
360
- COMMENT ON COLUMN site.menu_items.updated_at IS 'Timestamp when the menu item was last updated.';
361
- COMMENT ON COLUMN site.menu_items.created_by IS 'User who created the menu item.';
362
- COMMENT ON COLUMN site.menu_items.updated_by IS 'User who last updated the menu item.';
363
-
364
- CREATE INDEX if not exists idx_menu_items_menu_id ON site.menu_items(menu_id);
365
- CREATE INDEX if not exists idx_menu_items_parent_id ON site.menu_items(parent_id);
366
- CREATE INDEX if not exists idx_menu_items_order_index ON site.menu_items(order_index);
165
+ ALTER TABLE site.menus ADD CONSTRAINT menus_name_locale_unique UNIQUE (name, locale);
367
166
 
368
167
  -- drop table if exists site.roles cascade;
369
168
  CREATE TABLE if not exists site.roles (
@@ -421,26 +220,316 @@ CREATE INDEX if not exists idx_permissions_user_id ON site.permissions(user_id);
421
220
  CREATE INDEX if not exists idx_permissions_content_type_id ON site.permissions(content_type_id);
422
221
  CREATE INDEX if not exists idx_permissions_subject ON site.permissions(subject);
423
222
 
424
- -- DROP FUNCTION site.getmenu(text);
425
- CREATE OR REPLACE FUNCTION site.getMenu(_name text)
426
- RETURNS json AS
427
- $BODY$
428
- DECLARE
429
-
430
- BEGIN
431
-
432
- return (
433
- WITH RECURSIVE menu_tree AS (
434
- SELECT menu_item_id as id, parent_id, title, url, 0 AS level
435
- FROM site.menu_items
436
- WHERE parent_id IS NULL AND title = _name
437
- UNION ALL
438
- SELECT mi.menu_item_id, mi.parent_id, mi.title, mi.url, level + 1
439
- FROM site.menu_items mi
440
- INNER JOIN menu_tree mt ON mi.parent_id = mt.id
441
- )
442
- SELECT json_agg(row_to_json(menu_tree.*)) from menu_tree --ORDER BY level, id
223
+ create schema if not exists site;
224
+
225
+ CREATE TABLE if not exists site.tokens (
226
+ id text PRIMARY KEY default next_id(), -- Auto-incremented primary key
227
+ token_name TEXT NOT NULL, -- Display name of the token (e.g., "Contentful CLI", "test")
228
+ token_value TEXT NOT NULL, -- Partially masked actual token string
229
+ last_used TIMESTAMP NULL, -- When the token was last used (nullable)
230
+ created_at TIMESTAMP NOT NULL default now(), -- When the token was created
231
+ updated_at TIMESTAMP NOT NULL default now(), -- When the token was updated
232
+ created_by TEXT, -- Who created the token
233
+ updated_by TEXT, -- When updated the token
234
+ expiration_date TIMESTAMP, -- (nullable) Expiration date of the token
235
+ token_type TEXT CHECK (token_type IN ('CMA', 'PAT')), -- Type of token
236
+ token_status TEXT CHECK (token_status IN ('Active')), -- Current status
237
+ space_id TEXT NOT NULL -- site / portal
238
+ );
239
+
240
+ COMMENT ON TABLE site.tokens IS 'Stores metadata for access tokens including their usage, status, and scope.';
241
+
242
+ COMMENT ON COLUMN site.tokens.id IS 'Unique identifier for each token record.';
243
+ COMMENT ON COLUMN site.tokens.token_name IS 'Display name of the token (e.g., "Contentful CLI", "test").';
244
+ COMMENT ON COLUMN site.tokens.token_value IS 'Partially masked actual token string.';
245
+ COMMENT ON COLUMN site.tokens.last_used IS 'Timestamp of the last time the token was used.';
246
+ COMMENT ON COLUMN site.tokens.created_at IS 'Timestamp when the token was created.';
247
+ COMMENT ON COLUMN site.tokens.expiration_date IS 'Expiration timestamp, if set.';
248
+ COMMENT ON COLUMN site.tokens.token_type IS 'Type of token, such as CMA or PAT.';
249
+ COMMENT ON COLUMN site.tokens.token_status IS 'Current status of the token (e.g., Active).';
250
+ COMMENT ON COLUMN site.tokens.space_id IS 'Identifier for the associated space.';
251
+
252
+ CREATE TABLE if not exists site.spaces (
253
+ space_id text PRIMARY KEY default next_id(), -- Unique identifier for the space
254
+ name TEXT NOT NULL, -- Human-readable name of the space
255
+ created_at TIMESTAMP NOT NULL default now(), -- When the space was created
256
+ updated_at TIMESTAMP NOT NULL default now(), -- Last modification timestamp
257
+ created_by TEXT, -- Author
258
+ updated_by TEXT, -- Editor
259
+ organization_id text, -- ID of the parent organization (if part of an org)
260
+ default_locale TEXT not null default 'uk', -- Default language/locale of the space (e.g., "en", "ua" etc.)
261
+ locales text[],
262
+ plan TEXT CHECK (plan IN ('Free', 'Premium')) default 'Free' -- Plan type (e.g., "Free", "Premium")
263
+ );
264
+ insert into site.spaces(space_id, name) values('default','default') on conflict(space_id) do nothing;
265
+
266
+ COMMENT ON TABLE site.spaces IS 'Represents content spaces / sites';
267
+
268
+ COMMENT ON COLUMN site.spaces.space_id IS 'Unique identifier for the space';
269
+ COMMENT ON COLUMN site.spaces.name IS 'Human-readable name of the space';
270
+ COMMENT ON COLUMN site.spaces.created_at IS 'When the space was created';
271
+ COMMENT ON COLUMN site.spaces.updated_at IS 'Last modification timestamp';
272
+ COMMENT ON COLUMN site.spaces.organization_id IS 'ID of the parent organization (if part of an org)';
273
+ COMMENT ON COLUMN site.spaces.default_locale IS 'Default language/locale of the space (e.g., "en", "uk" etc.)';
274
+ COMMENT ON COLUMN site.spaces.plan IS 'Plan type (e.g., "Free", "Premium")';
275
+
276
+ alter table site.spaces alter column default_locale set default 'uk';
277
+
278
+ CREATE TABLE if not exists site.organizations (
279
+ organization_id TEXT PRIMARY KEY DEFAULT next_id(), -- Unique identifier for the organization
280
+ name TEXT NOT NULL, -- Name of the organization
281
+ created_at TIMESTAMP NOT NULL default now(), -- When the space was created
282
+ updated_at TIMESTAMP NOT NULL default now(), -- Last modification timestamp
283
+ created_by TEXT, -- Author
284
+ updated_by TEXT, -- Editor
285
+ owner_id TEXT NOT NULL, -- User ID of the organization owner
286
+ billing_email TEXT, -- Email used for billing-related communication (nullable)
287
+ plan TEXT NOT NULL DEFAULT 'Free' CHECK (plan IN ('Free', 'Pro', 'Enterprise')), -- Subscription plan
288
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'deleted')) -- Organization status
289
+ );
290
+
291
+ COMMENT ON TABLE site.organizations IS 'Organizations';
292
+
293
+ COMMENT ON COLUMN site.organizations.organization_id IS 'Unique identifier for the organization';
294
+ COMMENT ON COLUMN site.organizations.name IS 'Name of the organization';
295
+ COMMENT ON COLUMN site.organizations.created_at IS 'Timestamp when the organization was created';
296
+ COMMENT ON COLUMN site.organizations.owner_id IS 'User ID of the organization owner';
297
+ COMMENT ON COLUMN site.organizations.billing_email IS 'Email used for billing-related communication (nullable)';
298
+ COMMENT ON COLUMN site.organizations.plan IS 'Subscription plan (Free, Pro, Enterprise)';
299
+ COMMENT ON COLUMN site.organizations.status IS 'Status of the organization (active, suspended, deleted)';
300
+
301
+ CREATE TABLE if not exists site.comments (
302
+ comment_id text PRIMARY KEY DEFAULT next_id(), -- Unique identifier for the comment
303
+ content_id text NOT NULL, -- ID of the content item the comment is attached to
304
+ content_type TEXT NOT NULL, -- Type of the content (e.g., entry, asset, field, content_type)
305
+ author_id text NOT NULL, -- ID of the user who posted the comment
306
+ body TEXT NOT NULL, -- Comment text body
307
+ created_at TIMESTAMP NOT NULL DEFAULT now(), -- Timestamp when the comment was created
308
+ updated_at TIMESTAMP NOT NULL DEFAULT now(), -- Timestamp of last edit (nullable if never edited)
309
+ created_by TEXT, -- Author
310
+ updated_by TEXT, -- Editor
311
+ resolved BOOLEAN NOT NULL DEFAULT false, -- Whether the comment thread is marked as resolved
312
+ parent_id text, -- (nullable) ID of the parent comment, if this is a reply
313
+ thread_id text NOT NULL -- ID of the top-level thread this comment belongs to
443
314
  );
444
- END;
445
- $BODY$
446
- LANGUAGE plpgsql VOLATILE COST 100;
315
+
316
+ COMMENT ON COLUMN site.comments.comment_id IS 'Unique identifier for the comment';
317
+ COMMENT ON COLUMN site.comments.content_id IS 'ID of the content item the comment is attached to (entry, asset, etc.)';
318
+ COMMENT ON COLUMN site.comments.content_type IS 'Type of the content (e.g., entry, asset, field, content_type)';
319
+ COMMENT ON COLUMN site.comments.author_id IS 'ID of the user who posted the comment';
320
+ COMMENT ON COLUMN site.comments.body IS 'Comment text body';
321
+ COMMENT ON COLUMN site.comments.created_at IS 'Timestamp when the comment was created';
322
+ COMMENT ON COLUMN site.comments.updated_at IS 'Timestamp of last edit (nullable if never edited)';
323
+ COMMENT ON COLUMN site.comments.resolved IS 'Whether the comment thread is marked as resolved';
324
+ COMMENT ON COLUMN site.comments.parent_id IS 'ID of the parent comment if this is a reply';
325
+ COMMENT ON COLUMN site.comments.thread_id IS 'ID of the top-level thread this comment belongs to';
326
+
327
+ CREATE TABLE if not exists site.contents (
328
+ content_id text PRIMARY KEY default next_id(),
329
+ space_id text,
330
+ content_type_id text NOT NULL,
331
+ created_at TIMESTAMP NOT NULL DEFAULT now(),
332
+ updated_at TIMESTAMP NOT NULL DEFAULT now(),
333
+ published_at TIMESTAMP,
334
+ revision INTEGER NOT NULL DEFAULT 1,
335
+ locale TEXT NOT NULL DEFAULT 'uk'::text,
336
+ status TEXT CHECK (status IN ('draft', 'published', 'archived')) NOT NULL default 'draft',
337
+ slug TEXT,
338
+ title TEXT,
339
+ created_by text,
340
+ published_by text,
341
+ updated_by text,
342
+ meta json,
343
+ main_image text,
344
+ FOREIGN KEY (space_id) REFERENCES site.spaces(space_id),
345
+ FOREIGN KEY (content_type_id) REFERENCES site.content_types(content_type_id),
346
+ FOREIGN KEY (created_by) REFERENCES admin.users(uid),
347
+ FOREIGN KEY (published_by) REFERENCES admin.users(uid),
348
+ FOREIGN KEY (updated_by) REFERENCES admin.users(uid)
349
+ );
350
+
351
+ COMMENT ON COLUMN site.contents.content_id IS 'Unique identifier for the content item (entry)';
352
+ COMMENT ON COLUMN site.contents.space_id IS 'FK to the space this content belongs to';
353
+ COMMENT ON COLUMN site.contents.content_type_id IS 'FK to the content type definition';
354
+ COMMENT ON COLUMN site.contents.created_at IS 'Timestamp when the content was created';
355
+ COMMENT ON COLUMN site.contents.updated_at IS 'Timestamp of last update';
356
+ COMMENT ON COLUMN site.contents.published_at IS 'Timestamp of publication (nullable if not published)';
357
+ COMMENT ON COLUMN site.contents.revision IS 'Revision/version number';
358
+ COMMENT ON COLUMN site.contents.locale IS 'Language/locale for this content entry (e.g., en)';
359
+ COMMENT ON COLUMN site.contents.status IS 'Status of the entry (e.g., draft, published, archived)';
360
+ COMMENT ON COLUMN site.contents.slug IS 'URL-friendly string identifier';
361
+ COMMENT ON COLUMN site.contents.title IS 'Title of the content entry';
362
+ COMMENT ON COLUMN site.contents.created_by IS 'User who created the entry';
363
+ COMMENT ON COLUMN site.contents.published_by IS 'User who published the entry';
364
+ COMMENT ON COLUMN site.contents.updated_by IS 'User who last updated the entry';
365
+
366
+ CREATE TABLE if not exists site.content_data (
367
+ field_id text PRIMARY KEY DEFAULT next_id(),
368
+ content_id text NOT NULL,
369
+ object_id text NOT NULL,
370
+ field_key TEXT NOT NULL,
371
+ field_type TEXT NOT NULL,
372
+ field_value TEXT,
373
+ field_value_object json,
374
+ locale TEXT NOT NULL DEFAULT 'uk'::text,
375
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
376
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
377
+ created_by text,
378
+ updated_by text,
379
+
380
+ constraint content_data_content_id_fkey FOREIGN KEY (content_id) REFERENCES site.contents(content_id) on delete cascade
381
+ );
382
+
383
+ -- create unique index for slug
384
+ CREATE UNIQUE INDEX if not exists content_data_slug_unique ON site.content_data (field_value) WHERE field_key = 'slug';
385
+
386
+ COMMENT ON TABLE site.content_data IS 'Content fields';
387
+
388
+ COMMENT ON COLUMN site.content_data.field_id IS 'Unique identifier for the field entry';
389
+ COMMENT ON COLUMN site.content_data.content_id IS 'FK to Contents.content_id';
390
+ COMMENT ON COLUMN site.content_data.object_id IS 'Unique ID per content_id / field_key';
391
+ COMMENT ON COLUMN site.content_data.field_key IS 'Name of the field (e.g., title, description)';
392
+ COMMENT ON COLUMN site.content_data.field_type IS 'Field type (e.g., text, number, media, reference)';
393
+ COMMENT ON COLUMN site.content_data.field_value IS 'Data value stored as string';
394
+ COMMENT ON COLUMN site.content_data.field_value_object IS 'Data value stored as json';
395
+ COMMENT ON COLUMN site.content_data.locale IS 'Locale for this field (e.g., en-US, uk)';
396
+ COMMENT ON COLUMN site.content_data.created_at IS 'Field creation timestamp';
397
+ COMMENT ON COLUMN site.content_data.updated_at IS 'Last updated';
398
+ COMMENT ON COLUMN site.content_data.created_by IS 'Author';
399
+ COMMENT ON COLUMN site.content_data.updated_by IS 'Last editor';
400
+
401
+ alter table site.content_data alter column locale set default 'uk';
402
+ alter table site.contents alter column locale set default 'uk';
403
+
404
+
405
+ CREATE TABLE if not exists site.localization (
406
+ localization_id text PRIMARY KEY DEFAULT next_id(),
407
+ object_id text NOT NULL,
408
+ field_key TEXT NOT NULL,
409
+ field_value TEXT,
410
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
411
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
412
+ created_by text,
413
+ updated_by text,
414
+ constraint object_id_field_key_unique UNIQUE (object_id, field_key)
415
+ );
416
+
417
+ COMMENT ON TABLE site.localization IS 'Content localization';
418
+
419
+ COMMENT ON COLUMN site.localization.object_id IS 'Unique ID of content (content_data / custom data.table)';
420
+ COMMENT ON COLUMN site.localization.field_key IS 'Name of the field (e.g., title, description)';
421
+ COMMENT ON COLUMN site.localization.field_value IS 'Data value stored as string';
422
+ COMMENT ON COLUMN site.localization.created_at IS 'Field creation timestamp';
423
+ COMMENT ON COLUMN site.localization.updated_at IS 'Last updated';
424
+ COMMENT ON COLUMN site.localization.created_by IS 'Author';
425
+ COMMENT ON COLUMN site.localization.updated_by IS 'Last editor';
426
+
427
+ CREATE TABLE if not exists site.tags
428
+ (
429
+ tag_id text NOT NULL DEFAULT next_id(),
430
+ value text NOT NULL, -- Текстове значення тегу
431
+ slug text not null,
432
+ color text NOT NULL DEFAULT '#ababab'::text, -- Колір тегу
433
+ created_at timestamp without time zone DEFAULT now(),
434
+ updated_at timestamp without time zone,
435
+ created_by text,
436
+ updated_by text,
437
+ description text,
438
+ CONSTRAINT tags_pkey PRIMARY KEY (tag_id)
439
+ )
440
+ WITH (
441
+ OIDS=FALSE
442
+ );
443
+ ALTER TABLE site.tags
444
+ OWNER TO postgres;
445
+ COMMENT ON TABLE site.tags
446
+ IS 'Теги';
447
+ COMMENT ON COLUMN site.tags.value IS 'Текстове значення тегу';
448
+ COMMENT ON COLUMN site.tags.color IS 'Колір тегу';
449
+
450
+ alter table site.tags add column if not exists slug text;
451
+ COMMENT ON COLUMN site.tags.slug IS 'Alias тегу';
452
+ update site.tags set slug = tag_id where slug is null;
453
+ alter table site.tags alter column slug set not null;
454
+
455
+ CREATE TABLE if not exists site.tag_data
456
+ (
457
+ tag_content_id text NOT NULL DEFAULT next_id(),
458
+ tag_id text NOT NULL,
459
+ data_id text NOT NULL, -- Ідентифікатор будь якого контенту
460
+ created_at timestamp without time zone DEFAULT now(),
461
+ updated_at timestamp without time zone,
462
+ created_by text,
463
+ updated_by text,
464
+ CONSTRAINT tag_data_pkey PRIMARY KEY (tag_content_id),
465
+ CONSTRAINT tag_data_tag_id_fkey FOREIGN KEY (tag_id)
466
+ REFERENCES site.tags (tag_id) MATCH SIMPLE
467
+ ON UPDATE NO ACTION ON DELETE CASCADE,
468
+ CONSTRAINT tag_data_unique UNIQUE (tag_id, data_id)
469
+ );
470
+
471
+ ALTER TABLE site.tag_data drop constraint if exists tag_data_tag_id_fkey;
472
+ ALTER TABLE site.tag_data add CONSTRAINT tag_data_tag_id_fkey FOREIGN KEY (tag_id) REFERENCES site.tags (tag_id) MATCH SIMPLE ON DELETE CASCADE;
473
+ COMMENT ON TABLE site.tag_data
474
+ IS 'Відношення тегів до контенту';
475
+ COMMENT ON COLUMN site.tag_data.data_id IS 'Ідентифікатор будь якого контенту';
476
+
477
+ CREATE TABLE if not exists site.feedback
478
+ (
479
+ message_id text NOT NULL DEFAULT next_id(), -- ID
480
+ title text, -- Заголовок
481
+ email text NOT NULL, -- Електронна пошта
482
+ user_name text, -- ПІБ Повністю
483
+ message_text text, -- Повідомлення
484
+ feedback_type text NOT NULL DEFAULT 'message'::text, -- Тип
485
+ status text NOT NULL DEFAULT 'new'::text, -- Статус
486
+ phone text, -- Телефон
487
+ headers json,
488
+ resume_link text, -- Посилання на резюме
489
+ linkedin_link text, -- Посилання на лінкедин
490
+ portfolio_link text, -- Посилання на портфоліо
491
+ telegram_link text, -- Посилання на телеграм
492
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
493
+ updated_at timestamp with time zone NOT NULL DEFAULT now(),
494
+ created_by text,
495
+ updated_by text,
496
+ CONSTRAINT feedback_pkey PRIMARY KEY (message_id)
497
+ );
498
+
499
+ COMMENT ON COLUMN site.feedback.message_id IS 'ID';
500
+ COMMENT ON COLUMN site.feedback.title IS 'Заголовок';
501
+ COMMENT ON COLUMN site.feedback.email IS 'Електронна пошта';
502
+ COMMENT ON COLUMN site.feedback.user_name IS 'ПІБ Повністю';
503
+ COMMENT ON COLUMN site.feedback.message_text IS 'Повідомлення';
504
+ COMMENT ON COLUMN site.feedback.feedback_type IS 'Тип';
505
+ COMMENT ON COLUMN site.feedback.status IS 'Статус';
506
+ COMMENT ON COLUMN site.feedback.phone IS 'Телефон';
507
+ COMMENT ON COLUMN site.feedback.resume_link IS 'Посилання на резюме';
508
+ COMMENT ON COLUMN site.feedback.linkedin_link IS 'Посилання на лінкедин';
509
+ COMMENT ON COLUMN site.feedback.portfolio_link IS 'Посилання на портфоліо';
510
+ COMMENT ON COLUMN site.feedback.telegram_link IS 'Посилання на телеграм';
511
+
512
+ CREATE TABLE if not exists site.subscription
513
+ (
514
+ subscription_id text NOT NULL DEFAULT next_id(),
515
+ email text NOT NULL, -- Електронна пошта
516
+ enabled boolean DEFAULT true, -- Чи активна підписка
517
+ referer text, -- Звідки підписка
518
+ unsubscribed_at timestamp with time zone, -- Дата відписки
519
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
520
+ updated_at timestamp with time zone NOT NULL DEFAULT now(),
521
+ created_by text,
522
+ updated_by text,
523
+ CONSTRAINT subscription_pkey PRIMARY KEY (subscription_id),
524
+ CONSTRAINT subscription_email_unique UNIQUE (email)
525
+ );
526
+ COMMENT ON TABLE site.subscription
527
+ IS 'Підписки на новини';
528
+
529
+ alter table site.subscription add column if not exists referer text;
530
+ alter table site.subscription add column if not exists unsubscribed_at timestamp with time zone;
531
+
532
+ COMMENT ON COLUMN site.subscription.email IS 'Електронна пошта';
533
+ COMMENT ON COLUMN site.subscription.enabled IS 'Чи активна підписка';
534
+ COMMENT ON COLUMN site.subscription.referer IS 'Звідки підписка';
535
+ COMMENT ON COLUMN site.subscription.unsubscribed_at IS 'Дата відписки';