1use super::{
2 document_box::DocumentBoxScopeRaw,
3 folder::FolderId,
4 user::{User, UserId},
5};
6use crate::{
7 DbExecutor, DbResult,
8 models::{
9 document_box::DocumentBoxScopeRawRef,
10 shared::{
11 CountResult, DocboxInputPair, FolderPathSegment, WithFullPath, WithFullPathScope,
12 },
13 },
14};
15use chrono::{DateTime, Utc};
16use serde::Serialize;
17use sqlx::{postgres::PgQueryResult, prelude::FromRow};
18use utoipa::ToSchema;
19use uuid::Uuid;
20
21pub type LinkId = Uuid;
22
23#[derive(Debug, Clone, FromRow, Serialize, sqlx::Type, ToSchema)]
24#[sqlx(type_name = "docbox_link")]
25pub struct Link {
26 #[schema(value_type = Uuid)]
28 pub id: LinkId,
29 pub name: String,
31 pub value: String,
33 pub pinned: bool,
35 #[schema(value_type = Uuid)]
37 pub folder_id: FolderId,
38 pub created_at: DateTime<Utc>,
40 #[serde(skip)]
42 pub created_by: Option<UserId>,
43}
44
45impl Eq for Link {}
46
47impl PartialEq for Link {
48 fn eq(&self, other: &Self) -> bool {
49 self.id.eq(&other.id)
50 && self.name.eq(&other.name)
51 && self.value.eq(&other.value)
52 && self.pinned.eq(&other.pinned)
53 && self.folder_id.eq(&other.folder_id)
54 && self.created_by.eq(&self.created_by)
55 && self
58 .created_at
59 .timestamp_millis()
60 .eq(&other.created_at.timestamp_millis())
61 }
62}
63
64#[derive(Debug, FromRow, Serialize)]
65pub struct LinkWithScope {
66 #[sqlx(flatten)]
67 pub link: Link,
68 pub scope: String,
69}
70
71#[derive(Debug, Clone, Serialize, FromRow, ToSchema)]
72pub struct LinkWithExtra {
73 #[serde(flatten)]
74 pub link: Link,
75 #[schema(nullable, value_type = User)]
77 pub created_by: Option<User>,
78 #[schema(nullable, value_type = User)]
80 pub last_modified_by: Option<User>,
81 pub last_modified_at: Option<DateTime<Utc>>,
83}
84
85#[derive(Debug, FromRow, Serialize, ToSchema)]
87pub struct ResolvedLinkWithExtra {
88 #[serde(flatten)]
89 #[sqlx(flatten)]
90 pub link: LinkWithExtra,
91 pub full_path: Vec<FolderPathSegment>,
92}
93
94pub struct CreateLink {
95 pub name: String,
96 pub value: String,
97 pub folder_id: FolderId,
98 pub created_by: Option<UserId>,
99}
100
101impl Link {
102 pub async fn create(
103 db: impl DbExecutor<'_>,
104 CreateLink {
105 name,
106 value,
107 folder_id,
108 created_by,
109 }: CreateLink,
110 ) -> DbResult<Link> {
111 let id = Uuid::new_v4();
112 let created_at = Utc::now();
113
114 sqlx::query(
115 r#"INSERT INTO "docbox_links" (
116 "id",
117 "name",
118 "value",
119 "folder_id",
120 "created_by",
121 "created_at"
122 ) VALUES ($1, $2, $3, $4, $5, $6)
123 "#,
124 )
125 .bind(id)
126 .bind(name.clone())
127 .bind(value.clone())
128 .bind(folder_id)
129 .bind(created_by.as_ref())
130 .bind(created_at)
131 .execute(db)
132 .await?;
133
134 Ok(Link {
135 id,
136 name,
137 value,
138 folder_id,
139 created_by,
140 created_at,
141 pinned: false,
142 })
143 }
144
145 pub async fn move_to_folder(
146 mut self,
147 db: impl DbExecutor<'_>,
148 folder_id: FolderId,
149 ) -> DbResult<Link> {
150 sqlx::query(r#"UPDATE "docbox_links" SET "folder_id" = $1 WHERE "id" = $2"#)
151 .bind(folder_id)
152 .bind(self.id)
153 .execute(db)
154 .await?;
155
156 self.folder_id = folder_id;
157
158 Ok(self)
159 }
160
161 pub async fn rename(mut self, db: impl DbExecutor<'_>, name: String) -> DbResult<Link> {
162 sqlx::query(r#"UPDATE "docbox_links" SET "name" = $1 WHERE "id" = $2"#)
163 .bind(name.as_str())
164 .bind(self.id)
165 .execute(db)
166 .await?;
167
168 self.name = name;
169 Ok(self)
170 }
171
172 pub async fn set_pinned(mut self, db: impl DbExecutor<'_>, pinned: bool) -> DbResult<Link> {
173 sqlx::query(r#"UPDATE "docbox_links" SET "pinned" = $1 WHERE "id" = $2"#)
174 .bind(pinned)
175 .bind(self.id)
176 .execute(db)
177 .await?;
178
179 self.pinned = pinned;
180 Ok(self)
181 }
182
183 pub async fn update_value(mut self, db: impl DbExecutor<'_>, value: String) -> DbResult<Link> {
184 sqlx::query(r#"UPDATE "docbox_links" SET "value" = $1 WHERE "id" = $2"#)
185 .bind(value.as_str())
186 .bind(self.id)
187 .execute(db)
188 .await?;
189
190 self.value = value;
191
192 Ok(self)
193 }
194
195 pub async fn all(
196 db: impl DbExecutor<'_>,
197 offset: u64,
198 page_size: u64,
199 ) -> DbResult<Vec<LinkWithScope>> {
200 sqlx::query_as(
201 r#"
202 SELECT
203 "link".*,
204 "folder"."document_box" AS "scope"
205 FROM "docbox_links" AS "link"
206 INNER JOIN "docbox_folders" "folder" ON "link"."folder_id" = "folder"."id"
207 ORDER BY "link"."created_at" ASC
208 OFFSET $1
209 LIMIT $2
210 "#,
211 )
212 .bind(offset as i64)
213 .bind(page_size as i64)
214 .fetch_all(db)
215 .await
216 }
217
218 pub async fn find(
219 db: impl DbExecutor<'_>,
220 scope: DocumentBoxScopeRawRef<'_>,
221 link_id: LinkId,
222 ) -> DbResult<Option<Link>> {
223 sqlx::query_as(
224 r#"
225 SELECT "link".*
226 FROM "docbox_links" AS "link"
227 INNER JOIN "docbox_folders" "folder" ON "link"."folder_id" = "folder"."id"
228 WHERE "link"."id" = $1 AND "folder"."document_box" = $2
229 "#,
230 )
231 .bind(link_id)
232 .bind(scope)
233 .fetch_optional(db)
234 .await
235 }
236 pub async fn resolve_path(
239 db: impl DbExecutor<'_>,
240 link_id: LinkId,
241 ) -> DbResult<Vec<FolderPathSegment>> {
242 sqlx::query_as(r#"SELECT "id", "name" FROM resolve_link_path($1)"#)
243 .bind(link_id)
244 .fetch_all(db)
245 .await
246 }
247
248 pub async fn find_by_parent(
250 db: impl DbExecutor<'_>,
251 parent_id: FolderId,
252 ) -> DbResult<Vec<Link>> {
253 sqlx::query_as(r#"SELECT * FROM "docbox_links" WHERE "folder_id" = $1"#)
254 .bind(parent_id)
255 .fetch_all(db)
256 .await
257 }
258
259 pub async fn delete(&self, db: impl DbExecutor<'_>) -> DbResult<PgQueryResult> {
261 sqlx::query(r#"DELETE FROM "docbox_links" WHERE "id" = $1"#)
262 .bind(self.id)
263 .execute(db)
264 .await
265 }
266
267 pub async fn resolve_with_extra_mixed_scopes(
270 db: impl DbExecutor<'_>,
271 links_scope_with_id: Vec<DocboxInputPair<'_>>,
272 ) -> DbResult<Vec<WithFullPathScope<LinkWithExtra>>> {
273 if links_scope_with_id.is_empty() {
274 return Ok(Vec::new());
275 }
276
277 sqlx::query_as(r#"SELECT * FROM resolve_links_with_extra_mixed_scopes($1)"#)
278 .bind(links_scope_with_id)
279 .fetch_all(db)
280 .await
281 }
282
283 pub async fn resolve_with_extra(
286 db: impl DbExecutor<'_>,
287 scope: &DocumentBoxScopeRaw,
288 link_ids: Vec<Uuid>,
289 ) -> DbResult<Vec<WithFullPath<LinkWithExtra>>> {
290 if link_ids.is_empty() {
291 return Ok(Vec::new());
292 }
293
294 sqlx::query_as(r#"SELECT * FROM resolve_links_with_extra($1, $2)"#)
295 .bind(scope)
296 .bind(link_ids)
297 .fetch_all(db)
298 .await
299 }
300
301 pub async fn find_by_parent_with_extra(
303 db: impl DbExecutor<'_>,
304 parent_id: FolderId,
305 ) -> DbResult<Vec<LinkWithExtra>> {
306 sqlx::query_as(r#"SELECT * FROM resolve_links_by_parent_folder_with_extra($1)"#)
307 .bind(parent_id)
308 .fetch_all(db)
309 .await
310 }
311
312 pub async fn find_with_extra(
313 db: impl DbExecutor<'_>,
314 scope: &DocumentBoxScopeRaw,
315 link_id: LinkId,
316 ) -> DbResult<Option<LinkWithExtra>> {
317 sqlx::query_as(r#"SELECT * FROM resolve_link_by_id_with_extra($1, $2)"#)
318 .bind(scope)
319 .bind(link_id)
320 .fetch_optional(db)
321 .await
322 }
323
324 pub async fn total_count(db: impl DbExecutor<'_>) -> DbResult<i64> {
326 let count_result: CountResult =
327 sqlx::query_as(r#"SELECT COUNT(*) AS "count" FROM "docbox_links""#)
328 .fetch_one(db)
329 .await?;
330
331 Ok(count_result.count)
332 }
333}