Skip to main content

max / makenotwork

4.0 KB · 130 lines History Blame Raw
1 //! Custom-page drafts and live-source updates (Custom Pages, Phase 3).
2 //!
3 //! Drafts let a creator experiment without touching the live page. There is one
4 //! draft per `(owner, page_kind, page)`; the editor upserts it on every
5 //! keystroke (debounced) and the preview renders from it. Saving promotes the
6 //! draft to the live `custom_html`/`custom_css` columns and deletes the draft.
7
8 use sqlx::PgPool;
9 use uuid::Uuid;
10
11 use super::UserId;
12 use crate::error::Result;
13
14 /// `page_kind` for a profile draft.
15 pub const KIND_USER: &str = "user";
16 /// `page_kind` for a project draft.
17 pub const KIND_PROJECT: &str = "project";
18
19 /// A row from `custom_page_drafts`.
20 #[derive(Debug, Clone, sqlx::FromRow)]
21 pub struct CustomPageDraft {
22 pub id: Uuid,
23 pub owner_id: UserId,
24 pub page_kind: String,
25 pub page_id: Uuid,
26 pub custom_html: String,
27 pub custom_css: String,
28 }
29
30 /// Fetch a draft by its (capability) id. Used by the preview route.
31 pub async fn get_draft(pool: &PgPool, id: Uuid) -> Result<Option<CustomPageDraft>> {
32 let draft = sqlx::query_as::<_, CustomPageDraft>("SELECT * FROM custom_page_drafts WHERE id = $1")
33 .bind(id)
34 .fetch_optional(pool)
35 .await?;
36 Ok(draft)
37 }
38
39 /// Return the existing draft for this page, or create one seeded from the
40 /// current live source. The seed only applies on first creation -- an existing
41 /// in-progress draft is returned untouched so the creator resumes where they
42 /// left off.
43 pub async fn get_or_create_draft(
44 pool: &PgPool,
45 owner_id: UserId,
46 page_kind: &str,
47 page_id: Uuid,
48 seed_html: &str,
49 seed_css: &str,
50 ) -> Result<CustomPageDraft> {
51 let draft = sqlx::query_as::<_, CustomPageDraft>(
52 r#"
53 INSERT INTO custom_page_drafts (owner_id, page_kind, page_id, custom_html, custom_css)
54 VALUES ($1, $2, $3, $4, $5)
55 ON CONFLICT (owner_id, page_kind, page_id)
56 DO UPDATE SET updated_at = custom_page_drafts.updated_at
57 RETURNING *
58 "#,
59 )
60 .bind(owner_id)
61 .bind(page_kind)
62 .bind(page_id)
63 .bind(seed_html)
64 .bind(seed_css)
65 .fetch_one(pool)
66 .await?;
67 Ok(draft)
68 }
69
70 /// Write the draft's content (autosave). Upserts on the page key and returns the
71 /// stored row (so the caller has the stable draft id).
72 pub async fn upsert_draft(
73 pool: &PgPool,
74 owner_id: UserId,
75 page_kind: &str,
76 page_id: Uuid,
77 custom_html: &str,
78 custom_css: &str,
79 ) -> Result<CustomPageDraft> {
80 let draft = sqlx::query_as::<_, CustomPageDraft>(
81 r#"
82 INSERT INTO custom_page_drafts (owner_id, page_kind, page_id, custom_html, custom_css)
83 VALUES ($1, $2, $3, $4, $5)
84 ON CONFLICT (owner_id, page_kind, page_id)
85 DO UPDATE SET custom_html = EXCLUDED.custom_html,
86 custom_css = EXCLUDED.custom_css,
87 updated_at = now()
88 RETURNING *
89 "#,
90 )
91 .bind(owner_id)
92 .bind(page_kind)
93 .bind(page_id)
94 .bind(custom_html)
95 .bind(custom_css)
96 .fetch_one(pool)
97 .await?;
98 Ok(draft)
99 }
100
101 /// Delete a page's draft (after a successful save).
102 pub async fn delete_draft<'e>(
103 executor: impl sqlx::PgExecutor<'e>,
104 owner_id: UserId,
105 page_kind: &str,
106 page_id: Uuid,
107 ) -> Result<()> {
108 sqlx::query("DELETE FROM custom_page_drafts WHERE owner_id = $1 AND page_kind = $2 AND page_id = $3")
109 .bind(owner_id)
110 .bind(page_kind)
111 .bind(page_id)
112 .execute(executor)
113 .await?;
114 Ok(())
115 }
116
117 /// Delete drafts older than the given number of days (scheduled cleanup).
118 pub async fn delete_drafts_older_than(pool: &PgPool, days: i64) -> Result<u64> {
119 // Bind the interval rather than interpolating it: `days` is job-supplied
120 // today, but a bound `$1 * interval '1 day'` keeps this the one query in the
121 // module that can't drift into string interpolation.
122 let result = sqlx::query(
123 "DELETE FROM custom_page_drafts WHERE created_at < now() - ($1 * interval '1 day')",
124 )
125 .bind(days)
126 .execute(pool)
127 .await?;
128 Ok(result.rows_affected())
129 }
130