@oktis-works/cms 0.13.6 → 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.
@@ -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.1',
7
- '@oktis-works/admin': '0.23.2',
8
- '@oktis-works/web': '0.12.4',
9
- '@oktis-works/worker': '0.8.11',
10
- '@oktis-works/core': '0.14.1',
11
- '@oktis-works/plugin-runtime': '0.7.0',
12
- '@oktis-works/cms': '0.13.6',
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 (key) DO UPDATE SET
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 (key) DO UPDATE SET
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', 'Título', 'Título do post', true, '{}'::jsonb, 1, true, 'title'),
53
- ('slug', 'slug', 'slug', 'Slug', 'URL amigável (único)', true, '{}'::jsonb, 2, true, 'slug'),
54
- ('wysiwyg', 'content', 'content', 'Conteúdo', 'Corpo principal do post', true, '{}'::jsonb, 3, false, 'content'),
55
- ('textarea', 'excerpt', 'excerpt', 'Resumo', 'Resumo curto para listagens', false, '{}'::jsonb, 4, false, 'excerpt'),
56
- ('image', 'featured_image', 'featured_image', 'Imagem Destacada', 'Imagem principal do post', false, '{}'::jsonb, 5, false, 'featured_image'),
57
- ('text', 'seo_title', 'seo_title', 'SEO Title', 'Título para SEO (meta title)', false, '{}'::jsonb, 6, false, 'seo_title'),
58
- ('textarea', 'seo_description', 'seo_description', 'SEO Description', 'Descrição para SEO (meta description)', false, '{}'::jsonb, 7, false, 'seo_description'),
59
- ('select', 'status', 'status', 'Status', 'Status de publicação', true, '{"options":[{"value":"DRAFT","label":"Rascunho"},{"value":"PUBLISHED","label":"Publicado"},{"value":"ARCHIVED","label":"Arquivado"},{"value":"TRASHED","label":"Lixeira"}]}'::jsonb, 8, false, 'status'),
60
- ('user', 'author', 'author', 'Autor', 'Autor do post', true, '{}'::jsonb, 9, false, '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', 'title', 'Título', 'Título da página', true, '{}'::jsonb, 1, true, 'title'),
80
- ('slug', 'slug', 'slug', 'Slug', 'URL amigável (único)', true, '{}'::jsonb, 2, true, 'slug'),
81
- ('wysiwyg', 'content', 'content', 'Conteúdo', 'Corpo principal da página', true, '{}'::jsonb, 3, false, 'content'),
82
- ('textarea', 'excerpt', 'excerpt', 'Resumo', 'Resumo curto para listagens', false, '{}'::jsonb, 4, false, 'excerpt'),
83
- ('image', 'featured_image', 'featured_image', 'Imagem Destacada', 'Imagem principal da página', false, '{}'::jsonb, 5, false, 'featured_image'),
84
- ('text', 'seo_title', 'seo_title', 'SEO Title', 'Título para SEO (meta title)', false, '{}'::jsonb, 6, false, 'seo_title'),
85
- ('textarea', 'seo_description', 'seo_description', 'SEO Description', 'Descrição para SEO (meta description)', false, '{}'::jsonb, 7, false, 'seo_description'),
86
- ('select', 'status', 'status', 'Status', 'Status de publicação', true, '{"options":[{"value":"DRAFT","label":"Rascunho"},{"value":"PUBLISHED","label":"Publicado"},{"value":"ARCHIVED","label":"Arquivado"},{"value":"TRASHED","label":"Lixeira"}]}'::jsonb, 8, false, 'status'),
87
- ('user', 'author', 'author', 'Autor', 'Autor da página', true, '{}'::jsonb, 9, false, '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.6",
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.1",
23
+ "@oktis-works/auth": "^0.8.2",
24
24
  "@oktis-works/config": "^0.6.4",
25
- "@oktis-works/core": "^0.14.1",
26
- "@oktis-works/database": "^0.14.1",
27
- "@oktis-works/validation": "^0.7.0",
28
- "@oktis-works/theme-runtime": "^0.10.1"
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"