@oktis-works/cms 0.13.7 → 0.13.8
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/dist/generated/app-versions.js +7 -7
- package/migrations/V004__core__seed_default_field_groups.sql +24 -30
- package/migrations/V024__core__field_group_ownership_and_page_definitions.sql +64 -0
- package/migrations/V025__core__canonicalize_content_field_values.sql +97 -0
- package/package.json +6 -6
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// by `bun run build`. Used as the offline fallback for `okcms init` ranges
|
|
4
4
|
// when the npm registry cannot be reached.
|
|
5
5
|
export const APP_VERSIONS = {
|
|
6
|
-
'@oktis-works/api': '0.16.
|
|
7
|
-
'@oktis-works/admin': '0.
|
|
8
|
-
'@oktis-works/web': '0.
|
|
9
|
-
'@oktis-works/worker': '0.8.
|
|
10
|
-
'@oktis-works/core': '0.
|
|
11
|
-
'@oktis-works/plugin-runtime': '0.7.
|
|
12
|
-
'@oktis-works/cms': '0.13.
|
|
6
|
+
'@oktis-works/api': '0.16.3',
|
|
7
|
+
'@oktis-works/admin': '0.24.0',
|
|
8
|
+
'@oktis-works/web': '0.13.0',
|
|
9
|
+
'@oktis-works/worker': '0.8.12',
|
|
10
|
+
'@oktis-works/core': '0.15.0',
|
|
11
|
+
'@oktis-works/plugin-runtime': '0.7.2',
|
|
12
|
+
'@oktis-works/cms': '0.13.8',
|
|
13
13
|
};
|
|
14
14
|
//# sourceMappingURL=app-versions.js.map
|
|
@@ -12,11 +12,7 @@ WITH pg AS (
|
|
|
12
12
|
'{"content_type": ["post"]}'::jsonb,
|
|
13
13
|
'normal', 'standard', true, '{}'::jsonb
|
|
14
14
|
)
|
|
15
|
-
ON CONFLICT
|
|
16
|
-
title = EXCLUDED.title,
|
|
17
|
-
location_rules = EXCLUDED.location_rules,
|
|
18
|
-
active = EXCLUDED.active,
|
|
19
|
-
updated_at = NOW()
|
|
15
|
+
ON CONFLICT DO NOTHING
|
|
20
16
|
RETURNING id AS group_id
|
|
21
17
|
),
|
|
22
18
|
pa AS (
|
|
@@ -28,11 +24,7 @@ pa AS (
|
|
|
28
24
|
'{"content_type": ["page"]}'::jsonb,
|
|
29
25
|
'normal', 'standard', true, '{}'::jsonb
|
|
30
26
|
)
|
|
31
|
-
ON CONFLICT
|
|
32
|
-
title = EXCLUDED.title,
|
|
33
|
-
location_rules = EXCLUDED.location_rules,
|
|
34
|
-
active = EXCLUDED.active,
|
|
35
|
-
updated_at = NOW()
|
|
27
|
+
ON CONFLICT DO NOTHING
|
|
36
28
|
RETURNING id AS group_id
|
|
37
29
|
),
|
|
38
30
|
-- Inserir core fields para Post
|
|
@@ -49,15 +41,15 @@ pf AS (
|
|
|
49
41
|
true, v.is_locked, v.core_field_key
|
|
50
42
|
FROM pg
|
|
51
43
|
CROSS JOIN (VALUES
|
|
52
|
-
('text', 'title', 'title', '
|
|
53
|
-
('slug', 'slug', 'slug', 'Slug', 'URL
|
|
54
|
-
('wysiwyg', 'content', 'content', '
|
|
55
|
-
('textarea', 'excerpt', 'excerpt', '
|
|
56
|
-
('image', 'featured_image', 'featured_image', '
|
|
57
|
-
('text', 'seo_title', 'seo_title', 'SEO Title', '
|
|
58
|
-
('textarea', 'seo_description', 'seo_description', 'SEO Description', '
|
|
59
|
-
('select', 'status', 'status', 'Status', '
|
|
60
|
-
('user', 'author', 'author', '
|
|
44
|
+
('text', 'title', 'title', 'Title', 'Post title', true, '{}'::jsonb, 1, true, 'title'),
|
|
45
|
+
('slug', 'slug', 'slug', 'Slug', 'Unique public URL slug', true, '{}'::jsonb, 2, true, 'slug'),
|
|
46
|
+
('wysiwyg', 'content', 'content', 'Content', 'Main post content', true, '{}'::jsonb, 3, false, 'content'),
|
|
47
|
+
('textarea', 'excerpt', 'excerpt', 'Excerpt', 'Short summary for listings', false, '{}'::jsonb, 4, false, 'excerpt'),
|
|
48
|
+
('image', 'featured_image', 'featured_image', 'Featured Image', 'Primary post image', false, '{}'::jsonb, 5, false, 'featured_image'),
|
|
49
|
+
('text', 'seo_title', 'seo_title', 'SEO Title', 'SEO meta title', false, '{}'::jsonb, 6, false, 'seo_title'),
|
|
50
|
+
('textarea', 'seo_description', 'seo_description', 'SEO Description', 'SEO meta description', false, '{}'::jsonb, 7, false, 'seo_description'),
|
|
51
|
+
('select', 'status', 'status', 'Status', 'Publication status', true, '{"options":[{"value":"DRAFT","label":"Draft"},{"value":"PUBLISHED","label":"Published"},{"value":"ARCHIVED","label":"Archived"},{"value":"TRASHED","label":"Trash"}]}'::jsonb, 8, false, 'status'),
|
|
52
|
+
('user', 'author', 'author', 'Author', 'Post author', true, '{}'::jsonb, 9, false, 'author')
|
|
61
53
|
) AS v(type, name, key, label, instructions, required, config, sort_order, is_locked, core_field_key)
|
|
62
54
|
ON CONFLICT (key) DO NOTHING
|
|
63
55
|
RETURNING id
|
|
@@ -76,15 +68,15 @@ pa_f AS (
|
|
|
76
68
|
true, v.is_locked, v.core_field_key
|
|
77
69
|
FROM pa
|
|
78
70
|
CROSS JOIN (VALUES
|
|
79
|
-
('text', 'title', '
|
|
80
|
-
('slug', 'slug', '
|
|
81
|
-
('wysiwyg', 'content', '
|
|
82
|
-
('textarea', 'excerpt', '
|
|
83
|
-
('image', 'featured_image', '
|
|
84
|
-
('text', 'seo_title', '
|
|
85
|
-
('textarea', 'seo_description', '
|
|
86
|
-
('select', 'status', '
|
|
87
|
-
('user', 'author', '
|
|
71
|
+
('text', 'title', 'page_title', 'Title', 'Page title', true, '{}'::jsonb, 1, true, 'title'),
|
|
72
|
+
('slug', 'slug', 'page_slug', 'Slug', 'Unique public URL slug', true, '{}'::jsonb, 2, true, 'slug'),
|
|
73
|
+
('wysiwyg', 'content', 'page_content', 'Content', 'Main page content', true, '{}'::jsonb, 3, false, 'content'),
|
|
74
|
+
('textarea', 'excerpt', 'page_excerpt', 'Excerpt', 'Short summary for listings', false, '{}'::jsonb, 4, false, 'excerpt'),
|
|
75
|
+
('image', 'featured_image', 'page_featured_image', 'Featured Image', 'Primary page image', false, '{}'::jsonb, 5, false, 'featured_image'),
|
|
76
|
+
('text', 'seo_title', 'page_seo_title', 'SEO Title', 'SEO meta title', false, '{}'::jsonb, 6, false, 'seo_title'),
|
|
77
|
+
('textarea', 'seo_description', 'page_seo_description', 'SEO Description', 'SEO meta description', false, '{}'::jsonb, 7, false, 'seo_description'),
|
|
78
|
+
('select', 'status', 'page_status', 'Status', 'Publication status', true, '{"options":[{"value":"DRAFT","label":"Draft"},{"value":"PUBLISHED","label":"Published"},{"value":"ARCHIVED","label":"Archived"},{"value":"TRASHED","label":"Trash"}]}'::jsonb, 8, false, 'status'),
|
|
79
|
+
('user', 'author', 'page_author', 'Author', 'Page author', true, '{}'::jsonb, 9, false, 'author')
|
|
88
80
|
) AS v(type, name, key, label, instructions, required, config, sort_order, is_locked, core_field_key)
|
|
89
81
|
ON CONFLICT (key) DO NOTHING
|
|
90
82
|
RETURNING id
|
|
@@ -94,7 +86,9 @@ SELECT 1;
|
|
|
94
86
|
-- +migrate Down
|
|
95
87
|
DELETE FROM field_definitions WHERE key IN (
|
|
96
88
|
'title', 'slug', 'content', 'excerpt', 'featured_image',
|
|
97
|
-
'seo_title', 'seo_description', 'status', 'author'
|
|
89
|
+
'seo_title', 'seo_description', 'status', 'author',
|
|
90
|
+
'page_title', 'page_slug', 'page_content', 'page_excerpt', 'page_featured_image',
|
|
91
|
+
'page_seo_title', 'page_seo_description', 'page_status', 'page_author'
|
|
98
92
|
) AND is_core_field = true;
|
|
99
93
|
|
|
100
|
-
DELETE FROM field_groups WHERE key IN ('core-fields-post', 'core-fields-page');
|
|
94
|
+
DELETE FROM field_groups WHERE key IN ('core-fields-post', 'core-fields-page');
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- +migrate Up
|
|
2
|
+
-- Scope tenant field-group keys and repair page core definitions on upgrades.
|
|
3
|
+
|
|
4
|
+
ALTER TABLE field_groups DROP CONSTRAINT IF EXISTS field_groups_key_key;
|
|
5
|
+
CREATE UNIQUE INDEX IF NOT EXISTS field_groups_global_key_unique
|
|
6
|
+
ON field_groups (key) WHERE tenant_id IS NULL;
|
|
7
|
+
CREATE UNIQUE INDEX IF NOT EXISTS field_groups_tenant_key_unique
|
|
8
|
+
ON field_groups (tenant_id, key) WHERE tenant_id IS NOT NULL;
|
|
9
|
+
|
|
10
|
+
WITH page_group AS (
|
|
11
|
+
SELECT id
|
|
12
|
+
FROM field_groups
|
|
13
|
+
WHERE key = 'core-fields-page' AND tenant_id IS NULL
|
|
14
|
+
LIMIT 1
|
|
15
|
+
),
|
|
16
|
+
page_fields(type, name, key, label, instructions, required, config, sort_order, is_locked, core_field_key) AS (
|
|
17
|
+
VALUES
|
|
18
|
+
('text', 'title', 'page_title', 'Title', 'Page title', true, '{}'::jsonb, 1, true, 'title'),
|
|
19
|
+
('slug', 'slug', 'page_slug', 'Slug', 'Public URL slug', true, '{}'::jsonb, 2, true, 'slug'),
|
|
20
|
+
('wysiwyg', 'content', 'page_content', 'Content', 'Main page content', true, '{}'::jsonb, 3, false, 'content'),
|
|
21
|
+
('textarea', 'excerpt', 'page_excerpt', 'Excerpt', 'Short summary for listings', false, '{}'::jsonb, 4, false, 'excerpt'),
|
|
22
|
+
('image', 'featured_image', 'page_featured_image', 'Featured Image', 'Primary page image', false, '{}'::jsonb, 5, false, 'featured_image'),
|
|
23
|
+
('text', 'seo_title', 'page_seo_title', 'SEO Title', 'SEO title', false, '{}'::jsonb, 6, false, 'seo_title'),
|
|
24
|
+
('textarea', 'seo_description', 'page_seo_description', 'SEO Description', 'SEO description', false, '{}'::jsonb, 7, false, 'seo_description'),
|
|
25
|
+
('select', 'status', 'page_status', 'Status', 'Publication status', true, '{"options":[{"value":"DRAFT","label":"Draft"},{"value":"PUBLISHED","label":"Published"},{"value":"ARCHIVED","label":"Archived"},{"value":"TRASHED","label":"Trash"}]}'::jsonb, 8, false, 'status'),
|
|
26
|
+
('user', 'author', 'page_author', 'Author', 'Page author', true, '{}'::jsonb, 9, false, 'author')
|
|
27
|
+
)
|
|
28
|
+
INSERT INTO field_definitions (
|
|
29
|
+
group_id, parent_field_id, type, name, key, label, instructions,
|
|
30
|
+
required, config, sort_order, is_core_field, is_locked, core_field_key
|
|
31
|
+
)
|
|
32
|
+
SELECT page_group.id, NULL, page_fields.type, page_fields.name, page_fields.key,
|
|
33
|
+
page_fields.label, page_fields.instructions, page_fields.required,
|
|
34
|
+
page_fields.config, page_fields.sort_order, true,
|
|
35
|
+
page_fields.is_locked, page_fields.core_field_key
|
|
36
|
+
FROM page_group
|
|
37
|
+
CROSS JOIN page_fields
|
|
38
|
+
WHERE NOT EXISTS (
|
|
39
|
+
SELECT 1 FROM field_definitions existing
|
|
40
|
+
WHERE existing.group_id = page_group.id
|
|
41
|
+
AND existing.core_field_key = page_fields.core_field_key
|
|
42
|
+
)
|
|
43
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
44
|
+
group_id = EXCLUDED.group_id,
|
|
45
|
+
name = EXCLUDED.name,
|
|
46
|
+
label = EXCLUDED.label,
|
|
47
|
+
instructions = EXCLUDED.instructions,
|
|
48
|
+
required = EXCLUDED.required,
|
|
49
|
+
config = EXCLUDED.config,
|
|
50
|
+
sort_order = EXCLUDED.sort_order,
|
|
51
|
+
is_core_field = true,
|
|
52
|
+
is_locked = EXCLUDED.is_locked,
|
|
53
|
+
core_field_key = EXCLUDED.core_field_key,
|
|
54
|
+
updated_at = NOW();
|
|
55
|
+
|
|
56
|
+
-- +migrate Down
|
|
57
|
+
DROP INDEX IF EXISTS field_groups_tenant_key_unique;
|
|
58
|
+
DROP INDEX IF EXISTS field_groups_global_key_unique;
|
|
59
|
+
ALTER TABLE field_groups ADD CONSTRAINT field_groups_key_key UNIQUE (key);
|
|
60
|
+
DELETE FROM field_definitions
|
|
61
|
+
WHERE key IN ('page_title', 'page_slug', 'page_content', 'page_excerpt',
|
|
62
|
+
'page_featured_image', 'page_seo_title', 'page_seo_description',
|
|
63
|
+
'page_status', 'page_author')
|
|
64
|
+
AND is_core_field = true;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
-- +migrate Up
|
|
2
|
+
-- Make body the single canonical content document while preserving legacy
|
|
3
|
+
-- native columns, data, and relational field_values as read-only inputs.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS content_value_migration_conflicts (
|
|
6
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
7
|
+
content_id UUID NOT NULL REFERENCES content(id) ON DELETE CASCADE,
|
|
8
|
+
field_name VARCHAR(255) NOT NULL,
|
|
9
|
+
sources JSONB NOT NULL,
|
|
10
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
11
|
+
UNIQUE(content_id, field_name)
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_content_value_migration_conflicts_content
|
|
15
|
+
ON content_value_migration_conflicts(content_id);
|
|
16
|
+
|
|
17
|
+
CREATE OR REPLACE FUNCTION okcms_json_object(value jsonb) RETURNS jsonb
|
|
18
|
+
LANGUAGE plpgsql IMMUTABLE AS $$
|
|
19
|
+
BEGIN
|
|
20
|
+
IF value IS NULL THEN RETURN '{}'::jsonb; END IF;
|
|
21
|
+
IF jsonb_typeof(value) = 'object' THEN RETURN value; END IF;
|
|
22
|
+
IF jsonb_typeof(value) = 'string' THEN
|
|
23
|
+
BEGIN
|
|
24
|
+
RETURN (value #>> '{}')::jsonb;
|
|
25
|
+
EXCEPTION WHEN others THEN
|
|
26
|
+
RETURN '{}'::jsonb;
|
|
27
|
+
END;
|
|
28
|
+
END IF;
|
|
29
|
+
RETURN '{}'::jsonb;
|
|
30
|
+
END;
|
|
31
|
+
$$;
|
|
32
|
+
|
|
33
|
+
INSERT INTO content_value_migration_conflicts (content_id, field_name, sources)
|
|
34
|
+
SELECT c.id, keys.key,
|
|
35
|
+
jsonb_build_object('body', body_values->keys.key, 'data', data_values->keys.key)
|
|
36
|
+
FROM content c
|
|
37
|
+
CROSS JOIN LATERAL (SELECT okcms_json_object(c.body) AS body_values,
|
|
38
|
+
okcms_json_object(c.data) AS data_values) sources
|
|
39
|
+
CROSS JOIN LATERAL jsonb_object_keys(sources.body_values || sources.data_values) keys(key)
|
|
40
|
+
WHERE sources.body_values ? keys.key
|
|
41
|
+
AND sources.data_values ? keys.key
|
|
42
|
+
AND sources.body_values->keys.key IS DISTINCT FROM sources.data_values->keys.key
|
|
43
|
+
ON CONFLICT (content_id, field_name) DO NOTHING;
|
|
44
|
+
|
|
45
|
+
WITH relational AS (
|
|
46
|
+
SELECT fv.entity_id,
|
|
47
|
+
jsonb_object_agg(fd.name, fv.value) AS relational_values
|
|
48
|
+
FROM field_values fv
|
|
49
|
+
JOIN field_definitions fd ON fd.id = fv.field_definition_id
|
|
50
|
+
WHERE fv.entity_type = 'content'
|
|
51
|
+
GROUP BY fv.entity_id
|
|
52
|
+
), source_values AS (
|
|
53
|
+
SELECT c.id,
|
|
54
|
+
jsonb_build_object(
|
|
55
|
+
'title', c.title,
|
|
56
|
+
'slug', c.slug,
|
|
57
|
+
'excerpt', c.excerpt,
|
|
58
|
+
'featured_image', c.featured_image_id,
|
|
59
|
+
'seo_title', c.seo_title,
|
|
60
|
+
'seo_description', c.seo_description,
|
|
61
|
+
'status', c.status,
|
|
62
|
+
'author', c.author_id
|
|
63
|
+
) || okcms_json_object(c.data) || COALESCE(relational.relational_values, '{}'::jsonb) || okcms_json_object(c.body) AS logical_values
|
|
64
|
+
FROM content c
|
|
65
|
+
LEFT JOIN relational ON relational.entity_id = c.id
|
|
66
|
+
)
|
|
67
|
+
UPDATE content c
|
|
68
|
+
SET body = jsonb_strip_nulls(
|
|
69
|
+
source_values.logical_values
|
|
70
|
+
|| jsonb_build_object(
|
|
71
|
+
'content', COALESCE(source_values.logical_values->'content', source_values.logical_values->'html')
|
|
72
|
+
)
|
|
73
|
+
- 'html' - 'featuredImageId' - 'featured_image_id' - 'seoTitle' - 'seoDescription'
|
|
74
|
+
),
|
|
75
|
+
data = '{}'::jsonb
|
|
76
|
+
FROM source_values
|
|
77
|
+
WHERE source_values.id = c.id;
|
|
78
|
+
|
|
79
|
+
UPDATE content_versions v
|
|
80
|
+
SET body = jsonb_strip_nulls(
|
|
81
|
+
jsonb_build_object(
|
|
82
|
+
'title', v.title,
|
|
83
|
+
'slug', v.slug,
|
|
84
|
+
'excerpt', v.excerpt,
|
|
85
|
+
'featured_image', v.featured_image_id,
|
|
86
|
+
'seo_title', v.seo_title,
|
|
87
|
+
'seo_description', v.seo_description
|
|
88
|
+
) || okcms_json_object(v.body)
|
|
89
|
+
- 'html' - 'featuredImageId' - 'featured_image_id' - 'seoTitle' - 'seoDescription'
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
DROP FUNCTION okcms_json_object(jsonb);
|
|
93
|
+
|
|
94
|
+
-- +migrate Down
|
|
95
|
+
-- The canonicalization is intentionally one-way. Restore content and
|
|
96
|
+
-- content_versions from the database backup captured before this migration.
|
|
97
|
+
DROP TABLE IF EXISTS content_value_migration_conflicts;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oktis-works/cms",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
"clean": "rm -rf dist migrations tsconfig.tsbuildinfo"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@oktis-works/auth": "^0.8.
|
|
23
|
+
"@oktis-works/auth": "^0.8.2",
|
|
24
24
|
"@oktis-works/config": "^0.6.4",
|
|
25
|
-
"@oktis-works/core": "^0.
|
|
26
|
-
"@oktis-works/database": "^0.14.
|
|
27
|
-
"@oktis-works/validation": "^0.
|
|
28
|
-
"@oktis-works/theme-runtime": "^0.
|
|
25
|
+
"@oktis-works/core": "^0.15.0",
|
|
26
|
+
"@oktis-works/database": "^0.14.2",
|
|
27
|
+
"@oktis-works/validation": "^0.8.0",
|
|
28
|
+
"@oktis-works/theme-runtime": "^0.11.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"typescript": "5.7.3"
|