Skip to main content

max / makenotwork

4.0 KB · 133 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 =
33 sqlx::query_as::<_, CustomPageDraft>("SELECT * FROM custom_page_drafts WHERE id = $1")
34 .bind(id)
35 .fetch_optional(pool)
36 .await?;
37 Ok(draft)
38 }
39
40 /// Return the existing draft for this page, or create one seeded from the
41 /// current live source. The seed only applies on first creation, an existing
42 /// in-progress draft is returned untouched so the creator resumes where they
43 /// left off.
44 pub async fn get_or_create_draft(
45 pool: &PgPool,
46 owner_id: UserId,
47 page_kind: &str,
48 page_id: Uuid,
49 seed_html: &str,
50 seed_css: &str,
51 ) -> Result<CustomPageDraft> {
52 let draft = sqlx::query_as::<_, CustomPageDraft>(
53 r"
54 INSERT INTO custom_page_drafts (owner_id, page_kind, page_id, custom_html, custom_css)
55 VALUES ($1, $2, $3, $4, $5)
56 ON CONFLICT (owner_id, page_kind, page_id)
57 DO UPDATE SET updated_at = custom_page_drafts.updated_at
58 RETURNING *
59 ",
60 )
61 .bind(owner_id)
62 .bind(page_kind)
63 .bind(page_id)
64 .bind(seed_html)
65 .bind(seed_css)
66 .fetch_one(pool)
67 .await?;
68 Ok(draft)
69 }
70
71 /// Write the draft's content (autosave). Upserts on the page key and returns the
72 /// stored row (so the caller has the stable draft id).
73 pub async fn upsert_draft(
74 pool: &PgPool,
75 owner_id: UserId,
76 page_kind: &str,
77 page_id: Uuid,
78 custom_html: &str,
79 custom_css: &str,
80 ) -> Result<CustomPageDraft> {
81 let draft = sqlx::query_as::<_, CustomPageDraft>(
82 r"
83 INSERT INTO custom_page_drafts (owner_id, page_kind, page_id, custom_html, custom_css)
84 VALUES ($1, $2, $3, $4, $5)
85 ON CONFLICT (owner_id, page_kind, page_id)
86 DO UPDATE SET custom_html = EXCLUDED.custom_html,
87 custom_css = EXCLUDED.custom_css,
88 updated_at = now()
89 RETURNING *
90 ",
91 )
92 .bind(owner_id)
93 .bind(page_kind)
94 .bind(page_id)
95 .bind(custom_html)
96 .bind(custom_css)
97 .fetch_one(pool)
98 .await?;
99 Ok(draft)
100 }
101
102 /// Delete a page's draft (after a successful save).
103 pub async fn delete_draft<'e>(
104 executor: impl sqlx::PgExecutor<'e>,
105 owner_id: UserId,
106 page_kind: &str,
107 page_id: Uuid,
108 ) -> Result<()> {
109 sqlx::query(
110 "DELETE FROM custom_page_drafts WHERE owner_id = $1 AND page_kind = $2 AND page_id = $3",
111 )
112 .bind(owner_id)
113 .bind(page_kind)
114 .bind(page_id)
115 .execute(executor)
116 .await?;
117 Ok(())
118 }
119
120 /// Delete drafts older than the given number of days (scheduled cleanup).
121 pub async fn delete_drafts_older_than(pool: &PgPool, days: i64) -> Result<u64> {
122 // Bind the interval rather than interpolating it: `days` is job-supplied
123 // today, but a bound `$1 * interval '1 day'` keeps this the one query in the
124 // module that can't drift into string interpolation.
125 let result = sqlx::query(
126 "DELETE FROM custom_page_drafts WHERE created_at < now() - ($1 * interval '1 day')",
127 )
128 .bind(days)
129 .execute(pool)
130 .await?;
131 Ok(result.rows_affected())
132 }
133