Skip to main content

coil_cms/module/
core.rs

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}