Skip to main content

max / makenotwork

9.0 KB · 300 lines History Blame Raw
1 //! Items: create, read, update, publish, and the tags an item carries.
2
3 use super::{MnwApiClient, empty_response, json_response};
4 use serde::{Deserialize, Serialize};
5
6 /// Response from the create-item internal endpoint.
7 #[derive(Debug, Deserialize)]
8 #[allow(dead_code)]
9 pub(crate) struct ItemCreated {
10 pub item_id: String,
11 pub project_id: String,
12 }
13
14 /// Full item detail returned from the get/update endpoints.
15 #[derive(Debug, Clone, Deserialize, Serialize)]
16 pub(crate) struct ItemDetail {
17 pub id: String,
18 pub title: String,
19 pub description: Option<String>,
20 pub price_cents: i32,
21 pub item_type: String,
22 pub is_public: bool,
23 pub slug: String,
24 pub sort_order: i32,
25 pub sales_count: i32,
26 pub download_count: i32,
27 pub play_count: i32,
28 pub pwyw_enabled: bool,
29 pub pwyw_min_cents: Option<i32>,
30 pub has_audio: bool,
31 pub has_cover: bool,
32 pub created_at: String,
33 pub updated_at: String,
34 }
35
36 /// A version of an item.
37 #[derive(Debug, Clone, Deserialize, Serialize)]
38 #[allow(
39 clippy::struct_field_names,
40 reason = "version_number mirrors the server JSON/DB schema; renaming would diverge the field from the wire contract"
41 )]
42 pub(crate) struct Version {
43 pub id: String,
44 pub version_number: String,
45 pub changelog: Option<String>,
46 pub file_name: Option<String>,
47 pub file_size_bytes: Option<i64>,
48 pub download_count: i32,
49 pub is_current: bool,
50 pub created_at: String,
51 }
52
53 /// A tag on an item or from search.
54 #[derive(Debug, Clone, Deserialize, Serialize)]
55 pub(crate) struct TagInfo {
56 pub id: String,
57 pub name: String,
58 pub slug: String,
59 pub is_primary: bool,
60 }
61
62 impl MnwApiClient {
63 /// Create an item in a project.
64 pub(crate) async fn create_item(
65 &self,
66 user_id: &str,
67 project_id: &str,
68 title: &str,
69 item_type: &str,
70 price_cents: i32,
71 ) -> anyhow::Result<ItemCreated> {
72 let url = format!("{}/api/internal/creator/items", self.base_url);
73 let resp = self
74 .http
75 .post(&url)
76 .bearer_auth(&self.service_token)
77 .header("X-MNW-Actor", self.actor_header())
78 .json(&serde_json::json!({
79 "user_id": user_id,
80 "project_id": project_id,
81 "title": title,
82 "item_type": item_type,
83 "price_cents": price_cents,
84 }))
85 .send()
86 .await?;
87
88 json_response(resp, "create_item").await
89 }
90
91 /// Fetch full item detail.
92 pub(crate) async fn get_item_detail(
93 &self,
94 user_id: &str,
95 item_id: &str,
96 ) -> anyhow::Result<ItemDetail> {
97 let url = format!("{}/api/internal/creator/items/{}", self.base_url, item_id);
98 let resp = self
99 .http
100 .get(&url)
101 .bearer_auth(&self.service_token)
102 .header("X-MNW-Actor", self.actor_header())
103 .query(&[("user_id", user_id)])
104 .send()
105 .await?;
106
107 json_response(resp, "get_item_detail").await
108 }
109
110 /// Update item fields. Only non-None fields are changed.
111 pub(crate) async fn update_item(
112 &self,
113 user_id: &str,
114 item_id: &str,
115 title: Option<&str>,
116 description: Option<&str>,
117 price_cents: Option<i32>,
118 is_public: Option<bool>,
119 ) -> anyhow::Result<ItemDetail> {
120 let url = format!("{}/api/internal/creator/items/{}", self.base_url, item_id);
121 let mut body = serde_json::json!({ "user_id": user_id });
122 if let Some(t) = title {
123 body["title"] = serde_json::Value::String(t.to_string());
124 }
125 if let Some(d) = description {
126 body["description"] = serde_json::Value::String(d.to_string());
127 }
128 if let Some(p) = price_cents {
129 body["price_cents"] = serde_json::json!(p);
130 }
131 if let Some(v) = is_public {
132 body["is_public"] = serde_json::json!(v);
133 }
134
135 let resp = self
136 .http
137 .put(&url)
138 .bearer_auth(&self.service_token)
139 .header("X-MNW-Actor", self.actor_header())
140 .json(&body)
141 .send()
142 .await?;
143
144 json_response(resp, "update_item").await
145 }
146
147 /// Delete an item permanently.
148 pub(crate) async fn delete_item(&self, user_id: &str, item_id: &str) -> anyhow::Result<()> {
149 let url = format!("{}/api/internal/creator/items/{}", self.base_url, item_id);
150 let resp = self
151 .http
152 .delete(&url)
153 .bearer_auth(&self.service_token)
154 .header("X-MNW-Actor", self.actor_header())
155 .query(&[("user_id", user_id)])
156 .send()
157 .await?;
158
159 empty_response(resp, "delete_item").await
160 }
161
162 /// Publish an item (set is_public=true).
163 pub(crate) async fn publish_item(
164 &self,
165 user_id: &str,
166 item_id: &str,
167 ) -> anyhow::Result<ItemDetail> {
168 let url = format!(
169 "{}/api/internal/creator/items/{}/publish",
170 self.base_url, item_id
171 );
172 let resp = self
173 .http
174 .post(&url)
175 .bearer_auth(&self.service_token)
176 .header("X-MNW-Actor", self.actor_header())
177 .json(&serde_json::json!({ "user_id": user_id }))
178 .send()
179 .await?;
180
181 json_response(resp, "publish_item").await
182 }
183
184 /// Unpublish an item (set is_public=false).
185 pub(crate) async fn unpublish_item(
186 &self,
187 user_id: &str,
188 item_id: &str,
189 ) -> anyhow::Result<ItemDetail> {
190 let url = format!(
191 "{}/api/internal/creator/items/{}/unpublish",
192 self.base_url, item_id
193 );
194 let resp = self
195 .http
196 .post(&url)
197 .bearer_auth(&self.service_token)
198 .header("X-MNW-Actor", self.actor_header())
199 .json(&serde_json::json!({ "user_id": user_id }))
200 .send()
201 .await?;
202
203 json_response(resp, "unpublish_item").await
204 }
205
206 /// Fetch versions for an item.
207 pub(crate) async fn get_item_versions(
208 &self,
209 user_id: &str,
210 item_id: &str,
211 ) -> anyhow::Result<Vec<Version>> {
212 let url = format!(
213 "{}/api/internal/creator/items/{}/versions",
214 self.base_url, item_id
215 );
216 let resp = self
217 .http
218 .get(&url)
219 .bearer_auth(&self.service_token)
220 .header("X-MNW-Actor", self.actor_header())
221 .query(&[("user_id", user_id)])
222 .send()
223 .await?;
224
225 json_response(resp, "get_item_versions").await
226 }
227
228 pub(crate) async fn list_item_tags(
229 &self,
230 user_id: &str,
231 item_id: &str,
232 ) -> anyhow::Result<Vec<TagInfo>> {
233 let url = format!(
234 "{}/api/internal/creator/items/{}/tags",
235 self.base_url, item_id
236 );
237 let resp = self
238 .http
239 .get(&url)
240 .bearer_auth(&self.service_token)
241 .header("X-MNW-Actor", self.actor_header())
242 .query(&[("user_id", user_id)])
243 .send()
244 .await?;
245 json_response(resp, "list_item_tags").await
246 }
247
248 pub(crate) async fn search_tags(&self, query: &str) -> anyhow::Result<Vec<TagInfo>> {
249 let url = format!("{}/api/internal/tags/search", self.base_url);
250 let resp = self
251 .http
252 .get(&url)
253 .bearer_auth(&self.service_token)
254 .header("X-MNW-Actor", self.actor_header())
255 .query(&[("q", query)])
256 .send()
257 .await?;
258 json_response(resp, "search_tags").await
259 }
260
261 pub(crate) async fn add_item_tag(
262 &self,
263 user_id: &str,
264 item_id: &str,
265 tag_id: &str,
266 ) -> anyhow::Result<()> {
267 let url = format!("{}/api/internal/creator/items/tags", self.base_url);
268 let resp = self
269 .http
270 .post(&url)
271 .bearer_auth(&self.service_token)
272 .header("X-MNW-Actor", self.actor_header())
273 .json(&serde_json::json!({"user_id": user_id, "item_id": item_id, "tag_id": tag_id}))
274 .send()
275 .await?;
276 empty_response(resp, "add_item_tag").await
277 }
278
279 // Unused by the TUI today; kept so the client mirrors the full
280 // /api/internal surface rather than only the paths one caller happens to hit.
281 #[allow(dead_code)]
282 pub(crate) async fn remove_item_tag(
283 &self,
284 user_id: &str,
285 item_id: &str,
286 tag_id: &str,
287 ) -> anyhow::Result<()> {
288 let url = format!("{}/api/internal/creator/items/tags/remove", self.base_url);
289 let resp = self
290 .http
291 .post(&url)
292 .bearer_auth(&self.service_token)
293 .header("X-MNW-Actor", self.actor_header())
294 .json(&serde_json::json!({"user_id": user_id, "item_id": item_id, "tag_id": tag_id}))
295 .send()
296 .await?;
297 empty_response(resp, "remove_item_tag").await
298 }
299 }
300