1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct CmsModule {
5 pub(super) name: String,
6 pub(super) config_namespace: String,
7 pub(super) admin_resources: Vec<AdminResourceContribution>,
8}
9
10impl CmsModule {
11 pub fn new() -> Self {
12 Self {
13 name: "cms".to_string(),
14 config_namespace: "cms".to_string(),
15 admin_resources: vec![
16 AdminResourceContribution::new(
17 "cms.pages",
18 "/admin/cms/pages",
19 "Pages",
20 "Pages",
21 AdminNavigationSection::Content,
22 AdminContributionKind::ResourceIndex,
23 Capability::CmsPageRead,
24 ),
25 AdminResourceContribution::new(
26 "cms.navigation",
27 "/admin/cms/navigation",
28 "Navigation",
29 "Navigation",
30 AdminNavigationSection::Content,
31 AdminContributionKind::ResourceIndex,
32 Capability::CmsNavigationEdit,
33 ),
34 AdminResourceContribution::new(
35 "cms.media",
36 "/admin/cms/media",
37 "Media",
38 "Media",
39 AdminNavigationSection::Content,
40 AdminContributionKind::ResourceIndex,
41 Capability::AssetRead,
42 ),
43 ],
44 }
45 }
46
47 pub fn admin_resources(&self) -> &[AdminResourceContribution] {
48 &self.admin_resources
49 }
50
51 pub fn live_pages_query(&self, locale: Option<&str>) -> Result<CmsPageQuery, CmsModelError> {
52 let query = QuerySpec::new(
53 PageRequest::new(0, 50)?,
54 QueryContext {
55 locale: locale.map(str::to_owned),
56 principal_id: None,
57 publication_visibility: PublicationVisibility::PublishedOnly,
58 cache_scope: if locale.is_some() {
59 QueryCacheScope::LocaleScoped
60 } else {
61 QueryCacheScope::Public
62 },
63 },
64 )
65 .with_filter(QueryFilter::new(
66 "workflow_status",
67 FilterOperator::Eq,
68 vec![PageWorkflowStatus::Published.to_string()],
69 )?)
70 .with_sort(QuerySort::ascending("live_path")?);
71
72 Ok(CmsPageQuery { query })
73 }
74
75 pub fn editorial_queue_query(
76 &self,
77 principal_id: &str,
78 locale: Option<&str>,
79 ) -> Result<CmsPageQuery, CmsModelError> {
80 let query = QuerySpec::new(
81 PageRequest::new(0, 100)?,
82 QueryContext {
83 locale: locale.map(str::to_owned),
84 principal_id: Some(require_non_empty("principal_id", principal_id.to_string())?),
85 publication_visibility: PublicationVisibility::IncludeDrafts,
86 cache_scope: QueryCacheScope::UserScoped,
87 },
88 )
89 .with_filter(QueryFilter::new(
90 "workflow_status",
91 FilterOperator::In,
92 vec![
93 PageWorkflowStatus::DraftOnly.to_string(),
94 PageWorkflowStatus::Scheduled.to_string(),
95 PageWorkflowStatus::PublishedWithDraft.to_string(),
96 PageWorkflowStatus::PublishedWithScheduledDraft.to_string(),
97 ],
98 )?)
99 .with_sort(QuerySort::ascending("updated_at")?);
100
101 Ok(CmsPageQuery { query })
102 }
103
104 pub fn redirect_lookup_query(
105 &self,
106 path: &str,
107 locale: Option<&str>,
108 ) -> Result<RedirectLookupQuery, CmsModelError> {
109 let query = QuerySpec::new(
110 PageRequest::new(0, 1)?,
111 QueryContext {
112 locale: locale.map(str::to_owned),
113 principal_id: None,
114 publication_visibility: PublicationVisibility::PublishedOnly,
115 cache_scope: if locale.is_some() {
116 QueryCacheScope::LocaleScoped
117 } else {
118 QueryCacheScope::Public
119 },
120 },
121 )
122 .with_filter(QueryFilter::new(
123 "redirect_from",
124 FilterOperator::Eq,
125 vec![validate_path("redirect_lookup_path", path.to_string())?],
126 )?);
127
128 Ok(RedirectLookupQuery { query })
129 }
130
131 pub fn migration_plan(&self) -> Result<MigrationPlan, CmsModelError> {
132 let owner = MigrationOwner::Module(self.name.clone());
133 let mut plan = MigrationPlan::new();
134 plan.insert(
135 MigrationStep::new(
136 MigrationId::new("001_pages_revisions")?,
137 owner.clone(),
138 10,
139 "create cms pages, localized revisions, and seo metadata tables",
140 )?
141 .with_statement(
142 "CREATE TABLE IF NOT EXISTS cms_pages (page_id TEXT PRIMARY KEY, locale TEXT NOT NULL, title TEXT NOT NULL, slug TEXT NOT NULL, template TEXT NOT NULL, body_html TEXT NOT NULL, live_path TEXT NOT NULL, workflow_status TEXT NOT NULL, seo_title TEXT, seo_description TEXT, canonical_path TEXT, media_references TEXT NOT NULL DEFAULT '[]', source_system TEXT, source_key TEXT UNIQUE, import_batch_id TEXT, fingerprint TEXT NOT NULL, updated_at BIGINT NOT NULL)",
143 )?,
144 )?;
145 plan.insert(
146 MigrationStep::new(
147 MigrationId::new("002_navigation")?,
148 owner.clone(),
149 20,
150 "create navigation trees and navigation item adjacency tables",
151 )?
152 .with_statement(
153 "CREATE TABLE IF NOT EXISTS cms_navigation (navigation_id TEXT PRIMARY KEY, locale TEXT, payload TEXT NOT NULL, updated_at BIGINT NOT NULL)",
154 )?,
155 )?;
156 plan.insert(
157 MigrationStep::new(
158 MigrationId::new("003_redirects")?,
159 owner.clone(),
160 30,
161 "create redirect rules and route handoff tables",
162 )?
163 .with_statement(
164 "CREATE TABLE IF NOT EXISTS cms_redirects (redirect_from TEXT PRIMARY KEY, redirect_to TEXT NOT NULL, locale TEXT, permanent BOOLEAN NOT NULL)",
165 )?,
166 )?;
167 plan.insert(
168 MigrationStep::new(
169 MigrationId::new("004_publication_queue")?,
170 owner,
171 40,
172 "create scheduled publication queue and preview token tables",
173 )?
174 .with_statement(
175 "CREATE TABLE IF NOT EXISTS cms_publication_queue (page_id TEXT PRIMARY KEY, publish_at BIGINT NOT NULL)",
176 )?
177 .with_statement(
178 "CREATE TABLE IF NOT EXISTS cms_preview_tokens (token TEXT PRIMARY KEY, page_id TEXT NOT NULL, expires_at BIGINT NOT NULL)",
179 )?,
180 )?;
181 Ok(plan)
182 }
183}
184
185impl Default for CmsModule {
186 fn default() -> Self {
187 Self::new()
188 }
189}