Skip to main content

max / makenotwork

3.2 KB · 128 lines History Blame Raw
1 //! Item section CRUD: tabbed markdown content blocks within items.
2
3 use sqlx::PgPool;
4
5 use super::models::*;
6 use super::{ItemId, ItemSectionId};
7 use crate::error::Result;
8
9 /// List all sections for an item, ordered by sort_order.
10 #[tracing::instrument(skip_all)]
11 pub async fn list_by_item(pool: &PgPool, item_id: ItemId) -> Result<Vec<DbItemSection>> {
12 let sections = sqlx::query_as::<_, DbItemSection>(
13 "SELECT * FROM item_sections WHERE item_id = $1 ORDER BY sort_order LIMIT 500",
14 )
15 .bind(item_id)
16 .fetch_all(pool)
17 .await?;
18
19 Ok(sections)
20 }
21
22 /// Fetch a section by primary key. Returns `None` if not found.
23 #[tracing::instrument(skip_all)]
24 pub async fn get_by_id(pool: &PgPool, section_id: ItemSectionId) -> Result<Option<DbItemSection>> {
25 let section = sqlx::query_as::<_, DbItemSection>(
26 "SELECT * FROM item_sections WHERE id = $1",
27 )
28 .bind(section_id)
29 .fetch_optional(pool)
30 .await?;
31
32 Ok(section)
33 }
34
35 /// Insert a new section for an item.
36 #[tracing::instrument(skip_all)]
37 pub async fn create(
38 pool: &PgPool,
39 item_id: ItemId,
40 title: &str,
41 slug: &str,
42 body: &str,
43 sort_order: i32,
44 ) -> Result<DbItemSection> {
45 let section = sqlx::query_as::<_, DbItemSection>(
46 r#"
47 INSERT INTO item_sections (item_id, title, slug, body, sort_order)
48 VALUES ($1, $2, $3, $4, $5)
49 RETURNING *
50 "#,
51 )
52 .bind(item_id)
53 .bind(title)
54 .bind(slug)
55 .bind(body)
56 .bind(sort_order)
57 .fetch_one(pool)
58 .await?;
59
60 Ok(section)
61 }
62
63 /// Update a section's title, slug, and body.
64 #[tracing::instrument(skip_all)]
65 pub async fn update(
66 pool: &PgPool,
67 section_id: ItemSectionId,
68 title: &str,
69 slug: &str,
70 body: &str,
71 ) -> Result<DbItemSection> {
72 let section = sqlx::query_as::<_, DbItemSection>(
73 r#"
74 UPDATE item_sections
75 SET title = $2, slug = $3, body = $4, updated_at = now()
76 WHERE id = $1
77 RETURNING *
78 "#,
79 )
80 .bind(section_id)
81 .bind(title)
82 .bind(slug)
83 .bind(body)
84 .fetch_one(pool)
85 .await?;
86
87 Ok(section)
88 }
89
90 /// Permanently delete a section by ID.
91 #[tracing::instrument(skip_all)]
92 pub async fn delete(pool: &PgPool, section_id: ItemSectionId) -> Result<()> {
93 sqlx::query("DELETE FROM item_sections WHERE id = $1")
94 .bind(section_id)
95 .execute(pool)
96 .await?;
97
98 Ok(())
99 }
100
101 /// Reorder sections by setting sort_order from an ordered list of IDs.
102 #[tracing::instrument(skip_all)]
103 pub async fn reorder(pool: &PgPool, item_id: ItemId, section_ids: &[ItemSectionId]) -> Result<()> {
104 for (i, id) in section_ids.iter().enumerate() {
105 sqlx::query(
106 "UPDATE item_sections SET sort_order = $1, updated_at = now() WHERE id = $2 AND item_id = $3",
107 )
108 .bind(i as i32)
109 .bind(id)
110 .bind(item_id)
111 .execute(pool)
112 .await?;
113 }
114
115 Ok(())
116 }
117
118 /// Count sections for an item.
119 #[tracing::instrument(skip_all)]
120 pub async fn count_by_item(pool: &PgPool, item_id: ItemId) -> Result<i64> {
121 let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM item_sections WHERE item_id = $1")
122 .bind(item_id)
123 .fetch_one(pool)
124 .await?;
125
126 Ok(row.0)
127 }
128