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