1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use ts_rs::TS;
7use uuid::Uuid;
8
9use crate::{auth::PermissionLevel, user::UserSummary};
10
11use super::app::{AppCtx, AppError, AppState};
12
13pub async fn new_ref(ctx: AppCtx, content: Value) -> Result<Uuid, AppError> {
15 let ref_id = Uuid::now_v7();
16
17 let mut transaction = ctx.state.db.begin().await?;
18
19 let user_id = ctx.user.map(|user| user.user_id);
20 let insert_ref = sqlx::query!(
21 "
22 WITH snapshot AS (
23 INSERT INTO snapshots(for_ref, content, last_updated)
24 VALUES ($1, $2, NOW())
25 RETURNING id
26 )
27 INSERT INTO refs(id, head, created)
28 VALUES ($1, (SELECT id FROM snapshot), NOW())
29 ",
30 ref_id,
31 content
32 );
33 insert_ref.execute(&mut *transaction).await?;
34
35 let insert_permission = sqlx::query!(
36 "
37 INSERT INTO permissions(subject, object, level)
38 VALUES ($1, $2, 'own')
39 ",
40 user_id,
41 ref_id,
42 );
43 insert_permission.execute(&mut *transaction).await?;
44
45 transaction.commit().await?;
46 Ok(ref_id)
47}
48
49pub async fn head_snapshot(state: AppState, ref_id: Uuid) -> Result<Value, AppError> {
51 let query = sqlx::query!(
52 "
53 SELECT content FROM snapshots
54 WHERE id = (SELECT head FROM refs WHERE id = $1)
55 ",
56 ref_id
57 );
58 Ok(query.fetch_one(&state.db).await?.content)
59}
60
61pub async fn autosave(state: AppState, data: RefContent) -> Result<(), AppError> {
63 let RefContent { ref_id, content } = data;
64 let query = sqlx::query!(
65 "
66 UPDATE snapshots
67 SET content = $2, last_updated = NOW()
68 WHERE id = (SELECT head FROM refs WHERE id = $1)
69 ",
70 ref_id,
71 content
72 );
73 query.execute(&state.db).await?;
74 Ok(())
75}
76
77pub async fn save_snapshot(state: AppState, data: RefContent) -> Result<(), AppError> {
82 let RefContent { ref_id, content } = data;
83 let query = sqlx::query!(
84 "
85 WITH snapshot AS (
86 INSERT INTO snapshots(for_ref, content, last_updated)
87 VALUES ($1, $2, NOW())
88 RETURNING id
89 )
90 UPDATE refs
91 SET head = (SELECT id FROM snapshot)
92 WHERE id = $1
93 ",
94 ref_id,
95 content
96 );
97 query.execute(&state.db).await?;
98 Ok(())
99}
100
101pub async fn doc_id(state: AppState, ref_id: Uuid) -> Result<String, AppError> {
103 let automerge_io = &state.automerge_io;
104 let ack = automerge_io.emit_with_ack::<Vec<Option<String>>>("get_doc", ref_id).unwrap();
105 let mut response = ack.await?;
106
107 let maybe_doc_id = response.data.pop().flatten();
108 if let Some(doc_id) = maybe_doc_id {
109 Ok(doc_id)
111 } else {
112 let content = head_snapshot(state.clone(), ref_id).await?;
115 let data = RefContent { ref_id, content };
116 let ack = automerge_io.emit_with_ack::<Vec<String>>("create_doc", data).unwrap();
117 let response = ack.await?;
118 Ok(response.data[0].to_string())
119 }
120}
121
122#[derive(Debug, Serialize, Deserialize, TS)]
124pub struct RefContent {
125 #[serde(rename = "refId")]
126 pub ref_id: Uuid,
127 pub content: Value,
128}
129
130#[derive(Clone, Debug, Serialize, Deserialize, TS)]
134pub struct RefStub {
135 pub name: String,
136 #[serde(rename = "typeName")]
137 pub type_name: String,
138 #[serde(rename = "refId")]
139 pub ref_id: Uuid,
140 #[serde(rename = "permissionLevel")]
142 pub permission_level: PermissionLevel,
143 pub owner: Option<UserSummary>,
144 #[serde(rename = "createdAt")]
145 pub created_at: DateTime<Utc>,
146}
147
148#[derive(Clone, Debug, Serialize, Deserialize, TS)]
150pub struct RefQueryParams {
151 #[serde(rename = "ownerUsernameQuery")]
152 pub owner_username_query: Option<String>,
153 #[serde(rename = "refNameQuery")]
154 pub ref_name_query: Option<String>,
155 #[serde(rename = "searcherMinLevel")]
156 pub searcher_min_level: Option<PermissionLevel>,
157 #[serde(rename = "includePublicDocuments")]
158 pub include_public_documents: Option<bool>,
159 }
161
162pub async fn search_ref_stubs(
165 ctx: AppCtx,
166 search_params: RefQueryParams,
167) -> Result<Vec<RefStub>, AppError> {
168 let searcher_id = ctx.user.as_ref().map(|user| user.user_id.clone());
169
170 let min_level = search_params.searcher_min_level.unwrap_or(PermissionLevel::Read);
171
172 let results = sqlx::query!(
173 r#"
174 WITH effective_permissions AS (
175 /*
176 select at most one row per ref, the row is either:
177 - the searcher’s own permission, if it exists
178 - the public permission (subject IS NULL) when include_public_documents = TRUE and the
179 searcher does not already have a row
180 */
181 SELECT DISTINCT ON (object)
182 object,
183 level
184 FROM permissions
185 WHERE (subject = $1)
186 OR ($5 AND subject IS NULL)
187 ORDER BY object,
188 (subject IS NOT NULL) DESC -- prefer the user‑specific row
189 )
190 SELECT
191 refs.id AS ref_id,
192 snapshots.content->>'name' AS name,
193 snapshots.content->>'type' AS type_name,
194 refs.created as created_at,
195 effective_permissions.level AS "permission_level: PermissionLevel",
196 owner.id AS "owner_id?",
197 owner.username AS "owner_username?",
198 owner.display_name AS "owner_display_name?"
199 FROM refs
200 JOIN snapshots ON snapshots.id = refs.head
201 JOIN effective_permissions ON effective_permissions.object = refs.id
202 JOIN permissions AS p_owner
203 ON p_owner.object = refs.id AND p_owner.level = 'own'
204 LEFT JOIN users AS owner
205 ON owner.id = p_owner.subject
206 WHERE (
207 owner.username = $2
208 OR $2 IS NULL
209 )
210 AND (
211 snapshots.content->>'name' ILIKE '%' || $3 || '%'
212 OR $3 IS NULL
213 )
214 AND (
215 effective_permissions.level >= $4
216 )
217 LIMIT 100;
218 "#,
219 searcher_id,
220 search_params.owner_username_query,
221 search_params.ref_name_query,
222 min_level as PermissionLevel,
223 search_params.include_public_documents.unwrap_or(false),
224 )
225 .fetch_all(&ctx.state.db)
226 .await?;
227
228 let stubs = results
230 .into_iter()
231 .map(|row| RefStub {
232 ref_id: row.ref_id,
233 name: row.name.unwrap_or_else(|| "untitled".to_string()),
234 type_name: row.type_name.expect("type_name should never be null"),
235 permission_level: row.permission_level,
236 created_at: row.created_at,
237 owner: match row.owner_id {
238 Some(id) => Some(UserSummary {
239 id,
240 username: row.owner_username,
241 display_name: row.owner_display_name,
242 }),
243 _ => None,
244 },
245 })
246 .collect();
247
248 Ok(stubs)
249}