@opengis/cms 0.0.33 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/module/cms/cls/content.status.json +18 -0
- package/module/cms/cls/user_type.json +10 -0
- package/module/cms/form/admin.users.form.json +78 -0
- package/module/cms/select/cms.page_type.sql +2 -0
- package/module/cms/select/collection.sql +1 -0
- package/module/cms/select/news_tag_id.sql +12 -0
- package/module/cms/select/tag_id.sql +1 -0
- package/module/cms/table/admin.users.table.json +54 -0
- package/module/cms/table/collection.default.table.json +96 -0
- package/module/cms/table/single.default.table.json +114 -0
- package/package.json +6 -4
- package/plugin.js +1 -1
- package/server/migrations/fixes.sql +6 -5
- package/server/migrations/site.sql +54 -4
- package/server/plugins/vite.js +18 -30
- package/server/routes/cms/controllers/deleteContent.js +6 -7
- package/server/routes/cms/controllers/updateContent.js +16 -14
- package/server/routes/cms/utils/getSingle.js +2 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": {
|
|
3
|
+
"user_name": {
|
|
4
|
+
"type": "Text",
|
|
5
|
+
"rules": [
|
|
6
|
+
"required"
|
|
7
|
+
],
|
|
8
|
+
"ua": "Ім'я",
|
|
9
|
+
"i": "Вноситься ім'я користувача, що буде відображатися у системі"
|
|
10
|
+
},
|
|
11
|
+
"sur_name": {
|
|
12
|
+
"type": "Text",
|
|
13
|
+
"rules": [
|
|
14
|
+
"required"
|
|
15
|
+
],
|
|
16
|
+
"ua": "Прізвище",
|
|
17
|
+
"i": "Вноситься прізвище користувача, що буде відображатися у системі"
|
|
18
|
+
},
|
|
19
|
+
"father_name": {
|
|
20
|
+
"ua": "По-батькові",
|
|
21
|
+
"type": "Text"
|
|
22
|
+
},
|
|
23
|
+
"phone": {
|
|
24
|
+
"type": "Text",
|
|
25
|
+
"mask": "+389999999999",
|
|
26
|
+
"ua": "Телефон",
|
|
27
|
+
"i": "Вноситься телефон користувача"
|
|
28
|
+
},
|
|
29
|
+
"email": {
|
|
30
|
+
"type": "Text",
|
|
31
|
+
"ua": "E-mail",
|
|
32
|
+
"i": "Вноситься електронна адреса користувача",
|
|
33
|
+
"rules": [
|
|
34
|
+
"email"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"login": {
|
|
38
|
+
"type": "Text",
|
|
39
|
+
"ua": "Логін",
|
|
40
|
+
"i": "Вноситься довільний логін користувача латинськими літерами, що буде використовуватися для входу в систему",
|
|
41
|
+
"rules": [
|
|
42
|
+
"required"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"password": {
|
|
46
|
+
"type": "Text",
|
|
47
|
+
"ua": "Пароль",
|
|
48
|
+
"rules": [
|
|
49
|
+
"required",
|
|
50
|
+
{
|
|
51
|
+
"type": "regexp",
|
|
52
|
+
"pattern": "^.{8,}$",
|
|
53
|
+
"message": "Пароль повинен бути більше 8 символів"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"i": "Вноситься пароль, що буде використовуватися для входу в систему (рекомендоване використання складних паролів)"
|
|
57
|
+
},
|
|
58
|
+
"avatar": {
|
|
59
|
+
"type": "File",
|
|
60
|
+
"ua": "Аватар",
|
|
61
|
+
"i": "Додається зображення, що буде відображено в системі у якості аватара цього користувача"
|
|
62
|
+
},
|
|
63
|
+
"user_type": {
|
|
64
|
+
"type": "Select",
|
|
65
|
+
"ua": "Тип користувача",
|
|
66
|
+
"i": "Вноситься тип користувача",
|
|
67
|
+
"data": "user_type"
|
|
68
|
+
},
|
|
69
|
+
"enabled": {
|
|
70
|
+
"type": "Switcher",
|
|
71
|
+
"label": "Доступ"
|
|
72
|
+
},
|
|
73
|
+
"twofa": {
|
|
74
|
+
"type": "Switcher",
|
|
75
|
+
"label": "Двофакторна авторизація"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SELECT content_type_id, name FROM site.content_types where type='collection'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
select a.id, a.title, tags from data.news a
|
|
2
|
+
left join lateral(select array_agg(tag_id) as tags from site.tag_data b where a.id=b.data_id)b on 1=1
|
|
3
|
+
|
|
4
|
+
where
|
|
5
|
+
case when '{{parent}}'<> ''
|
|
6
|
+
then '{{parent}}'=any(b.tags)
|
|
7
|
+
else true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
and status='published'
|
|
11
|
+
|
|
12
|
+
order by a.updated_at
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
select tag_id, value from site.tags order by value
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ua": "Користувачі",
|
|
3
|
+
"key": "uid",
|
|
4
|
+
"order": "cdate desc",
|
|
5
|
+
"access": "admin",
|
|
6
|
+
"sqlColumns": "*",
|
|
7
|
+
"form": "admin.users.form",
|
|
8
|
+
"table": "admin.users",
|
|
9
|
+
"sql": [
|
|
10
|
+
{
|
|
11
|
+
"name": "social_auth_sql",
|
|
12
|
+
"sql": "select social_auth_obj,social_auth_date from admin.users_social_auth where uid=t.uid limit 1"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"sql": "SELECT coalesce(sur_name, '') || ' ' || coalesce(user_name, '') || ' ' || coalesce(father_name, '') as full_name FROM admin.users WHERE uid = t.uid",
|
|
16
|
+
"name": "full_name_sql"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"columns": [
|
|
20
|
+
{
|
|
21
|
+
"ua": "ПІБ",
|
|
22
|
+
"name": "full_name",
|
|
23
|
+
"format": "select",
|
|
24
|
+
"data": "full_name_sql"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"ua": "Тип користувача",
|
|
28
|
+
"name": "user_type_text",
|
|
29
|
+
"format": "select",
|
|
30
|
+
"data": "user_type_text"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"ua": "Статус доступу",
|
|
34
|
+
"name": "enabled"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"filters": [
|
|
38
|
+
{
|
|
39
|
+
"label": "Тип користувача",
|
|
40
|
+
"type": "Check",
|
|
41
|
+
"name": "user_type",
|
|
42
|
+
"data": "user_type"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"meta": {
|
|
46
|
+
"title": "full_name",
|
|
47
|
+
"search": "user_name,user_rnokpp"
|
|
48
|
+
},
|
|
49
|
+
"actions": [
|
|
50
|
+
"edit",
|
|
51
|
+
"del",
|
|
52
|
+
"add"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"table": "site.contents",
|
|
3
|
+
"columns": [
|
|
4
|
+
{
|
|
5
|
+
"label": "Slug",
|
|
6
|
+
"name": "slug",
|
|
7
|
+
"parent": "title",
|
|
8
|
+
"required": true,
|
|
9
|
+
"type": "slug"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"label": "Title",
|
|
13
|
+
"name": "title",
|
|
14
|
+
"required": true,
|
|
15
|
+
"type": "text",
|
|
16
|
+
"localization": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"label": "Status",
|
|
20
|
+
"name": "status",
|
|
21
|
+
"type": "select",
|
|
22
|
+
"data": "content.status",
|
|
23
|
+
"options": [
|
|
24
|
+
{
|
|
25
|
+
"id": "draft",
|
|
26
|
+
"text": "Draft"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "published",
|
|
30
|
+
"text": "Published"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "archived",
|
|
34
|
+
"text": "Archived"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "delayPublished",
|
|
38
|
+
"text": "Delay Published"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"required": true
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"label": "Publish at",
|
|
45
|
+
"name": "published_at",
|
|
46
|
+
"required": true,
|
|
47
|
+
"type": "datetime"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"label": "Зображення",
|
|
51
|
+
"name": "main_image",
|
|
52
|
+
"type": "mediaselect"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"filters": [
|
|
56
|
+
{
|
|
57
|
+
"extra": false,
|
|
58
|
+
"id": "status",
|
|
59
|
+
"name": "status",
|
|
60
|
+
"title": "Статус",
|
|
61
|
+
"type": "Check",
|
|
62
|
+
"data": "content.status",
|
|
63
|
+
"ua": "Статус"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"extra": false,
|
|
67
|
+
"label": "Publish at",
|
|
68
|
+
"name": "published_at",
|
|
69
|
+
"type": "Date"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"extra": false,
|
|
73
|
+
"name": "tag_list",
|
|
74
|
+
"type": "Tags",
|
|
75
|
+
"sql": "id in (select b.data_id from site.tags a left join site.tag_data b on a.tag_id=b.tag_id where a.tag_id=any($1))",
|
|
76
|
+
"label": "Теги"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"extra": false,
|
|
80
|
+
"columns": "slug,title",
|
|
81
|
+
"id": "search",
|
|
82
|
+
"name": "search",
|
|
83
|
+
"placeholder": "Пошук по тексту",
|
|
84
|
+
"title": "Пошук по тексту",
|
|
85
|
+
"type": "Text"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"extra": false,
|
|
89
|
+
"id": "created_at",
|
|
90
|
+
"name": "created_at",
|
|
91
|
+
"title": "Дата створення",
|
|
92
|
+
"type": "Date",
|
|
93
|
+
"ua": "Дата створення"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"table": "site.contents",
|
|
3
|
+
"columns": [
|
|
4
|
+
{
|
|
5
|
+
"label": "Slug",
|
|
6
|
+
"name": "slug",
|
|
7
|
+
"parent": "title",
|
|
8
|
+
"required": true,
|
|
9
|
+
"type": "slug"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"label": "Title",
|
|
13
|
+
"name": "title",
|
|
14
|
+
"required": true,
|
|
15
|
+
"type": "text",
|
|
16
|
+
"localization": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"label": "Status",
|
|
20
|
+
"name": "status",
|
|
21
|
+
"type": "select",
|
|
22
|
+
"data": "content.status",
|
|
23
|
+
"options": [
|
|
24
|
+
{
|
|
25
|
+
"id": "draft",
|
|
26
|
+
"text": "Draft"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "published",
|
|
30
|
+
"text": "Published"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "archived",
|
|
34
|
+
"text": "Archived"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "delayPublished",
|
|
38
|
+
"text": "Delay Published"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"required": true
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"label": "Publish at",
|
|
45
|
+
"name": "published_at",
|
|
46
|
+
"required": true,
|
|
47
|
+
"type": "datetime"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"label": "Контент",
|
|
51
|
+
"name": "single_body",
|
|
52
|
+
"type": "richtext",
|
|
53
|
+
"localization": true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "single_sections",
|
|
57
|
+
"label": "Секції",
|
|
58
|
+
"type": "reference",
|
|
59
|
+
"localization": true
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"label": "Зображення",
|
|
63
|
+
"name": "main_image",
|
|
64
|
+
"type": "mediaselect"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"filters": [
|
|
68
|
+
{
|
|
69
|
+
"extra": false,
|
|
70
|
+
"id": "status",
|
|
71
|
+
"name": "status",
|
|
72
|
+
"title": "Статус",
|
|
73
|
+
"type": "Check",
|
|
74
|
+
"data": "content.status",
|
|
75
|
+
"ua": "Статус"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"extra": false,
|
|
79
|
+
"label": "Publish at",
|
|
80
|
+
"name": "published_at",
|
|
81
|
+
"type": "Date"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"extra": false,
|
|
85
|
+
"label": "Контент",
|
|
86
|
+
"name": "single_body",
|
|
87
|
+
"type": "Text"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"extra": false,
|
|
91
|
+
"name": "tag_list",
|
|
92
|
+
"type": "Tags",
|
|
93
|
+
"sql": "id in (select b.data_id from site.tags a left join site.tag_data b on a.tag_id=b.tag_id where a.tag_id=any($1))",
|
|
94
|
+
"label": "Теги"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"extra": false,
|
|
98
|
+
"columns": "slug,title",
|
|
99
|
+
"id": "search",
|
|
100
|
+
"name": "search",
|
|
101
|
+
"placeholder": "Пошук по тексту",
|
|
102
|
+
"title": "Пошук по тексту",
|
|
103
|
+
"type": "Text"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"extra": false,
|
|
107
|
+
"id": "created_at",
|
|
108
|
+
"name": "created_at",
|
|
109
|
+
"title": "Дата створення",
|
|
110
|
+
"type": "Date",
|
|
111
|
+
"ua": "Дата створення"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengis/cms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.35",
|
|
4
4
|
"description": "cms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Softpro",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"license": "EULA",
|
|
9
9
|
"files": [
|
|
10
|
+
"module",
|
|
10
11
|
"dist",
|
|
11
12
|
"server",
|
|
12
13
|
"plugin.js"
|
|
@@ -15,8 +16,10 @@
|
|
|
15
16
|
"patch": "npm version patch && git push && npm publish",
|
|
16
17
|
"test": "node --test test/**/*.test.js",
|
|
17
18
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
19
|
+
"dev": "NODE_ENV=dev bun --env-file=.env.softpro server ",
|
|
20
|
+
"admin": "vite admin",
|
|
21
|
+
"build": "vite build && vite build admin",
|
|
22
|
+
"build:lib": "vite build",
|
|
20
23
|
"proxy": "vite dev admin",
|
|
21
24
|
"build-npm": "vite build",
|
|
22
25
|
"start": "bun --env-file=.env.ip server",
|
|
@@ -25,7 +28,6 @@
|
|
|
25
28
|
"prod": "NODE_ENV=production bun --env-file=.env.ip server ",
|
|
26
29
|
"prod-ip": "PORT=3019 node server --config=ip",
|
|
27
30
|
"prod-ip-test": "PORT=3025 node server --config=ip",
|
|
28
|
-
"dev": "node server --config=softpro",
|
|
29
31
|
"debug": "bun --watch server --config=softpro",
|
|
30
32
|
"ip-test": "node server --config=ip-test",
|
|
31
33
|
"ip": "node --env-file=.env.ip server",
|
package/plugin.js
CHANGED
|
@@ -39,7 +39,7 @@ export default async function (app) {
|
|
|
39
39
|
const pg = await getPGAsync(config.pg);
|
|
40
40
|
execMigrations(path.join(dirname, 'server/migrations'), pg).catch(err => console.log(err));
|
|
41
41
|
if (pg?.pk?.['site.content_types']) {
|
|
42
|
-
const customTables = await pg.query('select array_agg(table_name) from site.content_types where table_name is not null').then(el => el
|
|
42
|
+
const customTables = await pg.query('select array_agg(table_name) from site.content_types where table_name is not null').catch(err => console.log(err)).then(el => el?.rows?.[0]?.array_agg || []);
|
|
43
43
|
|
|
44
44
|
await Promise.all(customTables.filter(table => pg.pk?.[`data.${table}`])?.map(async (table) => {
|
|
45
45
|
await pg.query(`alter table data.${table} add column if not exists meta json`).catch(err => console.log(err));
|
|
@@ -42,12 +42,12 @@ left join lateral (select content_type_id from site.content_types b where a.type
|
|
|
42
42
|
-- seamless migration finish
|
|
43
43
|
|
|
44
44
|
--- insert default content types start
|
|
45
|
-
insert into site.content_types(content_type_id,name,title,type,columns)
|
|
45
|
+
insert into site.content_types(content_type_id,name,title,type,columns,template,uid)
|
|
46
46
|
values('pages','pages','Сторінки','collection',
|
|
47
47
|
'[{"name":"slug","label":"Slug","type":"slug","parent":"title","required":true},{"name":"title","label":"Title","type":"text","required":true},{"name":"status","label":"Status","type":"select","options":[{"id":"draft","text":"Draft"},{"id":"published","text":"Published"},{"id":"archived","text":"Archived"},{"id":"delayPublished","text":"Delay Published"}],"required":true},{"name":"published_at","label":"Publish at","type":"datetime","required":true}]'::json
|
|
48
|
-
) on conflict(content_type_id) do update set columns=excluded.columns, type = excluded.type;
|
|
49
|
-
insert into site.contents(content_id,content_type_id,space_id)
|
|
50
|
-
values('pages','pages','default') on conflict(content_id) do nothing;
|
|
48
|
+
,'','1') on conflict(content_type_id) do update set columns=excluded.columns, type = excluded.type;
|
|
49
|
+
insert into site.contents(content_id,content_type_id,space_id,template,uid)
|
|
50
|
+
values('pages','pages','default','','1') on conflict(content_id) do nothing;
|
|
51
51
|
--- insert default content types finish
|
|
52
52
|
|
|
53
53
|
alter table site.contents add column if not exists meta json;
|
|
@@ -83,7 +83,8 @@ update site.content_types
|
|
|
83
83
|
set columns=replace(columns::text,'publish_at', 'published_at')::json
|
|
84
84
|
where columns::text like '%publish_at%';
|
|
85
85
|
|
|
86
|
-
ALTER TABLE site.contents DROP CONSTRAINT contents_status_check;
|
|
86
|
+
ALTER TABLE site.contents DROP CONSTRAINT if exists contents_status_check;
|
|
87
|
+
-- select * from site.contents where not status = ANY (ARRAY['draft'::text, 'published'::text, 'archived'::text, 'delayPublished'::text])
|
|
87
88
|
ALTER TABLE site.contents ADD CONSTRAINT contents_status_check CHECK (status = ANY (ARRAY['draft'::text, 'published'::text, 'archived'::text, 'delayPublished'::text]));
|
|
88
89
|
|
|
89
90
|
UPDATE site.content_types
|
|
@@ -64,7 +64,7 @@ CREATE TABLE if not exists site.content_types (
|
|
|
64
64
|
table_name VARCHAR(50),
|
|
65
65
|
columns json,
|
|
66
66
|
status VARCHAR (20) DEFAULT 'draft' CHECK (status::text = ANY (ARRAY['draft', 'published', 'archived']::text[])),
|
|
67
|
-
visible BOOLEAN DEFAULT TRUE,
|
|
67
|
+
visible BOOLEAN DEFAULT TRUE,
|
|
68
68
|
localized BOOLEAN DEFAULT FALSE,
|
|
69
69
|
type VARCHAR(20) NOT NULL DEFAULT 'collection' CHECK (type::text = ANY (ARRAY['collection', 'single']::text[])),
|
|
70
70
|
schema JSONB,
|
|
@@ -78,6 +78,20 @@ CREATE TABLE if not exists site.content_types (
|
|
|
78
78
|
updated_by text REFERENCES admin.users(uid)
|
|
79
79
|
);
|
|
80
80
|
|
|
81
|
+
ALTER TABLE site.content_types add COLUMN if not exists table_name text;
|
|
82
|
+
ALTER TABLE site.content_types add COLUMN if not exists title text;
|
|
83
|
+
ALTER TABLE site.content_types add COLUMN if not exists status text;
|
|
84
|
+
ALTER TABLE site.content_types add COLUMN if not exists visible boolean default true;
|
|
85
|
+
ALTER TABLE site.content_types add COLUMN if not exists localized boolean default false;
|
|
86
|
+
ALTER TABLE site.content_types add COLUMN if not exists schema jsonb;
|
|
87
|
+
ALTER TABLE site.content_types add COLUMN if not exists icon text;
|
|
88
|
+
ALTER TABLE site.content_types add COLUMN if not exists color text;
|
|
89
|
+
ALTER TABLE site.content_types add COLUMN if not exists created_at TIMESTAMP DEFAULT NOW();
|
|
90
|
+
ALTER TABLE site.content_types add COLUMN if not exists updated_at TIMESTAMP DEFAULT NOW();
|
|
91
|
+
ALTER TABLE site.content_types add COLUMN if not exists created_by text REFERENCES admin.users(uid);
|
|
92
|
+
ALTER TABLE site.content_types add COLUMN if not exists updated_by text REFERENCES admin.users(uid);
|
|
93
|
+
ALTER TABLE site.content_types add COLUMN if not exists preview_path text;
|
|
94
|
+
|
|
81
95
|
ALTER TABLE if exists site.content_types ALTER COLUMN table_name DROP NOT NULL;
|
|
82
96
|
ALTER TABLE if exists site.content_types ADD COLUMN if not exists columns json;
|
|
83
97
|
|
|
@@ -145,6 +159,14 @@ CREATE TABLE if not exists site.menus (
|
|
|
145
159
|
);
|
|
146
160
|
|
|
147
161
|
alter table site.menus add column if not exists locale VARCHAR(20) not null default 'uk';
|
|
162
|
+
alter table site.menus add column if not exists content text;
|
|
163
|
+
alter table site.menus add column if not exists items json;
|
|
164
|
+
alter table site.menus add column if not exists description text;
|
|
165
|
+
ALTER TABLE site.menus add COLUMN if not exists created_at TIMESTAMP DEFAULT NOW();
|
|
166
|
+
ALTER TABLE site.menus add COLUMN if not exists updated_at TIMESTAMP DEFAULT NOW();
|
|
167
|
+
ALTER TABLE site.menus add COLUMN if not exists created_by text REFERENCES admin.users(uid);
|
|
168
|
+
ALTER TABLE site.menus add COLUMN if not exists updated_by text REFERENCES admin.users(uid);
|
|
169
|
+
|
|
148
170
|
ALTER TABLE site.menus DROP CONSTRAINT if exists menus_name_key;
|
|
149
171
|
ALTER TABLE site.menus DROP CONSTRAINT if exists menus_name_locale_unique;
|
|
150
172
|
alter table site.menus drop column if exists lang;
|
|
@@ -189,8 +211,8 @@ CREATE INDEX if not exists idx_roles_name ON site.roles(name);
|
|
|
189
211
|
-- drop table if exists site.permissions cascade;
|
|
190
212
|
CREATE TABLE if not exists site.permissions (
|
|
191
213
|
permission_id text PRIMARY KEY default next_id(),
|
|
192
|
-
role_id text references site.roles(role_id),
|
|
193
|
-
user_id text references admin.users(uid),
|
|
214
|
+
role_id text references site.roles(role_id),
|
|
215
|
+
user_id text references admin.users(uid),
|
|
194
216
|
content_type_id text references site.content_types(content_type_id),
|
|
195
217
|
subject VARCHAR(100) NOT NULL,
|
|
196
218
|
actions TEXT[] NOT NULL check (actions && (ARRAY['read', 'create', 'delete', 'edit']::text[])),
|
|
@@ -229,7 +251,7 @@ CREATE TABLE if not exists site.tokens (
|
|
|
229
251
|
last_used TIMESTAMP NULL, -- When the token was last used (nullable)
|
|
230
252
|
created_at TIMESTAMP NOT NULL default now(), -- When the token was created
|
|
231
253
|
updated_at TIMESTAMP NOT NULL default now(), -- When the token was updated
|
|
232
|
-
created_by TEXT, -- Who created the token
|
|
254
|
+
created_by TEXT, -- Who created the token
|
|
233
255
|
updated_by TEXT, -- When updated the token
|
|
234
256
|
expiration_date TIMESTAMP, -- (nullable) Expiration date of the token
|
|
235
257
|
token_type TEXT CHECK (token_type IN ('CMA', 'PAT')), -- Type of token
|
|
@@ -348,6 +370,19 @@ CREATE TABLE if not exists site.contents (
|
|
|
348
370
|
FOREIGN KEY (updated_by) REFERENCES admin.users(uid)
|
|
349
371
|
);
|
|
350
372
|
|
|
373
|
+
alter table site.contents add column if not exists space_id text;
|
|
374
|
+
alter table site.contents add column if not exists content_type_id text;
|
|
375
|
+
ALTER TABLE site.contents add COLUMN if not exists created_at TIMESTAMP DEFAULT NOW();
|
|
376
|
+
ALTER TABLE site.contents add COLUMN if not exists updated_at TIMESTAMP DEFAULT NOW();
|
|
377
|
+
ALTER TABLE site.contents add COLUMN if not exists created_by text REFERENCES admin.users(uid);
|
|
378
|
+
ALTER TABLE site.contents add COLUMN if not exists updated_by text REFERENCES admin.users(uid);
|
|
379
|
+
ALTER TABLE site.contents add COLUMN if not exists published_at TIMESTAMP DEFAULT NOW();
|
|
380
|
+
ALTER TABLE site.contents add COLUMN if not exists revision INTEGER NOT NULL DEFAULT 1;
|
|
381
|
+
ALTER TABLE site.contents add COLUMN if not exists locale TEXT NOT NULL DEFAULT 'uk'::text;
|
|
382
|
+
ALTER TABLE site.contents add COLUMN if not exists published_by text;
|
|
383
|
+
ALTER TABLE site.contents add COLUMN if not exists slug text;
|
|
384
|
+
ALTER TABLE site.contents add COLUMN if not exists meta json;
|
|
385
|
+
|
|
351
386
|
COMMENT ON COLUMN site.contents.content_id IS 'Unique identifier for the content item (entry)';
|
|
352
387
|
COMMENT ON COLUMN site.contents.space_id IS 'FK to the space this content belongs to';
|
|
353
388
|
COMMENT ON COLUMN site.contents.content_type_id IS 'FK to the content type definition';
|
|
@@ -380,6 +415,21 @@ CREATE TABLE if not exists site.content_data (
|
|
|
380
415
|
constraint content_data_content_id_fkey FOREIGN KEY (content_id) REFERENCES site.contents(content_id) on delete cascade
|
|
381
416
|
);
|
|
382
417
|
|
|
418
|
+
|
|
419
|
+
alter table site.content_data add column if not exists field_key text not null;
|
|
420
|
+
alter table site.content_data add column if not exists field_type text;
|
|
421
|
+
alter table site.content_data add column if not exists field_value text;
|
|
422
|
+
alter table site.content_data add column if not exists field_value_object json;
|
|
423
|
+
alter table site.content_data add column if not exists locale text;
|
|
424
|
+
alter table site.content_data add column if not exists field_id text;
|
|
425
|
+
alter table site.content_data add column if not exists object_id text;
|
|
426
|
+
ALTER TABLE site.content_data add COLUMN if not exists created_at TIMESTAMP DEFAULT NOW();
|
|
427
|
+
ALTER TABLE site.content_data add COLUMN if not exists updated_at TIMESTAMP DEFAULT NOW();
|
|
428
|
+
ALTER TABLE site.content_data add COLUMN if not exists created_by text REFERENCES admin.users(uid);
|
|
429
|
+
ALTER TABLE site.content_data add COLUMN if not exists updated_by text REFERENCES admin.users(uid);
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
383
433
|
-- create unique index for slug
|
|
384
434
|
CREATE UNIQUE INDEX if not exists content_data_slug_unique ON site.content_data (field_value) WHERE field_key = 'slug';
|
|
385
435
|
|
package/server/plugins/vite.js
CHANGED
|
@@ -1,58 +1,46 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import path
|
|
3
|
-
import {
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createServer } from 'vite';
|
|
4
4
|
|
|
5
5
|
import { config } from '@opengis/fastify-table/utils.js';
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
const root = `${dir}/../..`;
|
|
9
|
-
const adminRoot = path.join(root, 'admin');
|
|
10
|
-
|
|
11
|
-
const isProduction = process.env.NODE_ENV === 'production' || config.production;
|
|
7
|
+
const isProduction = process.env.NODE_ENV !== 'dev';
|
|
12
8
|
console.log({ isProduction })
|
|
13
9
|
async function plugin(fastify) {
|
|
14
10
|
// vite server
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
server: {
|
|
22
|
-
middlewareMode: true,
|
|
23
|
-
},
|
|
24
|
-
});
|
|
11
|
+
const viteServer = !isProduction ? await createServer({
|
|
12
|
+
root: 'admin',
|
|
13
|
+
server: {
|
|
14
|
+
middlewareMode: true,
|
|
15
|
+
},
|
|
16
|
+
}) : null;
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// hot relaod template
|
|
18
|
+
if (!isProduction) {
|
|
19
|
+
const dir1 = process.cwd();
|
|
20
|
+
viteServer.watcher.add(dir1);
|
|
30
21
|
viteServer.watcher.on('all', (d, t) => {
|
|
31
22
|
if (!t.includes('module')) return;
|
|
32
23
|
console.log(d, t);
|
|
33
24
|
viteServer.ws.send({ type: 'full-reload' });
|
|
34
25
|
});
|
|
35
26
|
|
|
36
|
-
// this is middleware for vite's dev
|
|
27
|
+
// this is middleware for vite's dev server
|
|
37
28
|
fastify.addHook('onRequest', async (req, reply) => {
|
|
38
29
|
const { user } = req.session?.passport || {};
|
|
39
|
-
|
|
40
|
-
return reply.redirect('/login');
|
|
41
|
-
}
|
|
30
|
+
// console.log(req.url);
|
|
42
31
|
|
|
43
32
|
const next = () => new Promise((resolve) => {
|
|
44
33
|
viteServer.middlewares(req.raw, reply.raw, () => resolve());
|
|
45
34
|
});
|
|
35
|
+
|
|
46
36
|
await next();
|
|
47
37
|
});
|
|
48
|
-
fastify.get('*', async () => { });
|
|
49
|
-
return;
|
|
50
38
|
}
|
|
51
39
|
|
|
52
40
|
// From Build
|
|
53
41
|
fastify.get('*', async (req, reply) => {
|
|
54
42
|
const { user } = req.session?.passport || {};
|
|
55
|
-
const indexPath = path.join(
|
|
43
|
+
const indexPath = path.join('admin/dist', 'index.html');
|
|
56
44
|
if (!user && config.pg && !req.url.startsWith('/src/') && !config.auth?.disable) {
|
|
57
45
|
return reply.redirect('/login');
|
|
58
46
|
}
|
|
@@ -65,8 +53,8 @@ async function plugin(fastify) {
|
|
|
65
53
|
|
|
66
54
|
const fileSize = {};
|
|
67
55
|
async function staticFile(req, reply) {
|
|
68
|
-
const assetsDir = '/dist';
|
|
69
|
-
const filePath = path.join(
|
|
56
|
+
const assetsDir = 'admin/dist';
|
|
57
|
+
const filePath = path.join( assetsDir, req.url);
|
|
70
58
|
const ext = path.extname(filePath);
|
|
71
59
|
|
|
72
60
|
if (!fs.existsSync(filePath)) return { status: 404, message: 'not found' };
|
|
@@ -18,24 +18,23 @@ export default async function deleteContent(req, reply) {
|
|
|
18
18
|
return reply.status(400).send('not enough params: id');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
21
|
if (id === 'pages') {
|
|
23
22
|
return reply.status(403).send('access restricted: pages contents cannot be deleted');
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
const { ctid, dbtable } = await pg.query('select content_type_id as ctid, table_name as dbtable from site.content_types where $1 in (content_type_id, name)', [params.type])
|
|
26
|
+
.then(el => el.rows?.[0] || {});
|
|
27
|
+
|
|
26
28
|
const arr = config.pg ? await pg.query(`select array_agg(relname)::text[] from pg_class a
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
left join pg_namespace b on a.relnamespace=b.oid
|
|
30
|
+
where a.relam=2 and b.nspname='data'`).then(el => el.rows?.[0]?.array_agg || []) : [];
|
|
29
31
|
|
|
30
|
-
if (!arr.length) {
|
|
32
|
+
if (!arr.length && type !== 'pages') {
|
|
31
33
|
return reply.status(400).send('empty schema: data');
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
const table = arr.find(el => el === params.type);
|
|
35
37
|
|
|
36
|
-
const { ctid, dbtable } = await pg.query('select content_type_id as ctid, table_name as dbtable from site.content_types where $1 in (content_type_id, name)', [params.type])
|
|
37
|
-
.then(el => el.rows?.[0] || {});
|
|
38
|
-
|
|
39
38
|
// singletone with extra at site.content_data
|
|
40
39
|
if (!table && !dbtable && ctid) {
|
|
41
40
|
const { cid, status } = ctid === 'pages' && id
|
|
@@ -47,20 +47,19 @@ export default async function updateContent(req, reply) {
|
|
|
47
47
|
return reply.status(400).send('empty body');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// order priority - custom columns -> default for pages
|
|
51
|
+
const { ctid, ctname, dbtable, columns: contentColumns = [] } = await pg.query('select content_type_id as ctid, name as ctname, table_name as dbtable, columns from site.content_types where content_type_id in (select content_type_id from site.contents where content_id=$1) or content_type_id=$2 order by content_type_id = \'pages\'', [id, type]).then(el => el.rows?.[0] || {});
|
|
52
|
+
|
|
50
53
|
const arr = config.pg ? await pg.query(`select array_agg(relname)::text[] from pg_class a
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
left join pg_namespace b on a.relnamespace=b.oid
|
|
55
|
+
where a.relam=2 and b.nspname='data'`).then(el => el.rows?.[0]?.array_agg || []) : [];
|
|
53
56
|
|
|
54
|
-
if (!arr.length) {
|
|
57
|
+
if (!arr.length && type !== 'pages') {
|
|
55
58
|
return reply.status(400).send('empty schema: data');
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
const table = arr.find(el => el === params.type);
|
|
59
62
|
|
|
60
|
-
// order priority - custom columns -> default for pages
|
|
61
|
-
const { ctid, ctname, dbtable, columns: contentColumns = [] } = await pg.query('select content_type_id as ctid, name as ctname, table_name as dbtable, columns from site.content_types where content_type_id in (select content_type_id from site.contents where content_id=$1) or content_type_id=$2 order by content_type_id = \'pages\'', [id, type]).then(el => el.rows?.[0] || {});
|
|
62
|
-
|
|
63
|
-
|
|
64
63
|
const loadTable = type === 'pages' ? await getTemplate('table', 'single.default.table') : {};
|
|
65
64
|
const columns = type === 'pages'
|
|
66
65
|
? (loadTable?.columns || []).concat(contentColumns.filter(col => loadTable?.columns.findIndex(el => el.name === col.name) === -1))
|
|
@@ -96,13 +95,14 @@ export default async function updateContent(req, reply) {
|
|
|
96
95
|
return reply.status(404).send('contents not found');
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
// ? deprecated, blocks if only default columns provided, therefore commented for now
|
|
99
|
+
// const contentId = cid === 'pages'
|
|
100
|
+
// ? await pg.query('select content_id from site.content_data where object_id=$1', [id]).then(el => el.rows?.[0]?.content_id)
|
|
101
|
+
// : cid;
|
|
102
102
|
|
|
103
|
-
if (!contentId) {
|
|
104
|
-
|
|
105
|
-
}
|
|
103
|
+
// if (!contentId) {
|
|
104
|
+
// return reply.status(404).send('contents not found: 2');
|
|
105
|
+
// }
|
|
106
106
|
|
|
107
107
|
const columnList = columns?.map?.(el => el.name) || [];
|
|
108
108
|
const types = columns?.reduce?.((acc, curr) => ({ ...acc, [curr.name]: curr.type || 'text' }), {}) || {};
|
|
@@ -133,7 +133,9 @@ export default async function updateContent(req, reply) {
|
|
|
133
133
|
data: { ...body, content_type_id: ctid1 },
|
|
134
134
|
uid: user?.uid,
|
|
135
135
|
});
|
|
136
|
-
|
|
136
|
+
// if (contentId) {
|
|
137
|
+
// await client.query(`delete from site.content_data where content_id=$1`, [contentId]);
|
|
138
|
+
// }
|
|
137
139
|
const objectId = (ctname === 'pages' || ['single', 'pages'].includes(type)) && id ? id : cid;
|
|
138
140
|
await client.query(`delete from site.content_data where object_id=$1`, [objectId]);
|
|
139
141
|
await Promise.all(keys.map(async key => dataInsert({
|
|
@@ -101,6 +101,7 @@ export default async function getSingle({
|
|
|
101
101
|
and ${localeQuery || '1=1'}
|
|
102
102
|
and ${contentQuery || 'true'}
|
|
103
103
|
and ${id ? `$2 in (slug, content_id)` : 'true'}
|
|
104
|
+
and content_id<>'pages'
|
|
104
105
|
order by case when status='archived' then true else false end, published_at desc nulls last
|
|
105
106
|
LIMIT ${limit} OFFSET ${id ? 0 : offset}
|
|
106
107
|
`;
|
|
@@ -120,6 +121,7 @@ export default async function getSingle({
|
|
|
120
121
|
and $1=$1 and $2=$2 and ${contentQuery || 'true'}
|
|
121
122
|
)
|
|
122
123
|
and ${statusQuery || 'true'}
|
|
124
|
+
and content_id<>'pages'
|
|
123
125
|
`, [contentId, id]).then(el => el.rows?.[0] || {});
|
|
124
126
|
|
|
125
127
|
const { rows = [] } = await pg.query(q, [contentId, id || '']);
|