Skip to main content

cloudillo_types/
meta_adapter.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! Adapter that manages metadata. Everything including tenants, profiles, actions, file metadata, etc.
5
6/// Special parent_id value for trashed files
7pub const TRASH_PARENT_ID: &str = "__trash__";
8
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11use serde_with::skip_serializing_none;
12use std::{cmp::Ordering, collections::HashMap, fmt::Debug};
13
14use crate::{
15	prelude::*,
16	types::{serialize_timestamp_iso, serialize_timestamp_iso_opt},
17};
18
19// Tenants, profiles
20//*******************
21#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
22pub enum ProfileType {
23	#[serde(rename = "person")]
24	Person,
25	#[serde(rename = "community")]
26	Community,
27}
28
29#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
30pub enum ProfileStatus {
31	#[serde(rename = "A")]
32	Active,
33	#[serde(rename = "T")]
34	Trusted,
35	#[serde(rename = "B")]
36	Blocked,
37	#[serde(rename = "M")]
38	Muted,
39	#[serde(rename = "S")]
40	Suspended,
41	#[serde(rename = "X")]
42	Banned,
43}
44
45#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
46pub enum ProfileConnectionStatus {
47	#[default]
48	Disconnected,
49	RequestPending,
50	Connected,
51}
52
53impl ProfileConnectionStatus {
54	pub fn is_connected(&self) -> bool {
55		matches!(self, ProfileConnectionStatus::Connected)
56	}
57}
58
59impl std::fmt::Display for ProfileConnectionStatus {
60	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61		match self {
62			ProfileConnectionStatus::Disconnected => write!(f, "disconnected"),
63			ProfileConnectionStatus::RequestPending => write!(f, "pending"),
64			ProfileConnectionStatus::Connected => write!(f, "connected"),
65		}
66	}
67}
68
69// Reference / Bookmark types
70//*****************************
71
72#[skip_serializing_none]
73#[derive(Debug, Clone, Serialize)]
74#[serde(rename_all = "camelCase")]
75pub struct RefData {
76	pub ref_id: Box<str>,
77	pub r#type: Box<str>,
78	pub description: Option<Box<str>>,
79	#[serde(serialize_with = "serialize_timestamp_iso")]
80	pub created_at: Timestamp,
81	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
82	pub expires_at: Option<Timestamp>,
83	/// Usage count: None = unlimited, Some(n) = n uses remaining
84	pub count: Option<u32>,
85	/// Resource ID for share links (e.g., file_id for share.file type)
86	pub resource_id: Option<Box<str>>,
87	/// Access level for share links ('R'=Read, 'W'=Write)
88	pub access_level: Option<char>,
89	/// Launch params as serialized query string (e.g., "mode=present")
90	pub params: Option<Box<str>>,
91}
92
93pub struct ListRefsOptions {
94	pub typ: Option<String>,
95	pub filter: Option<String>, // 'active', 'used', 'expired', 'all'
96	/// Filter by resource_id (for listing share links for a specific resource)
97	pub resource_id: Option<String>,
98}
99
100#[derive(Default)]
101pub struct CreateRefOptions {
102	pub typ: String,
103	pub description: Option<String>,
104	pub expires_at: Option<Timestamp>,
105	pub count: Option<u32>,
106	/// Resource ID for share links (e.g., file_id for share.file type)
107	pub resource_id: Option<String>,
108	/// Access level for share links ('R'=Read, 'W'=Write)
109	pub access_level: Option<char>,
110	/// Launch params as serialized query string (e.g., "mode=present")
111	pub params: Option<String>,
112}
113
114#[skip_serializing_none]
115#[derive(Debug, Serialize)]
116#[serde(rename_all = "camelCase")]
117pub struct Tenant<S: AsRef<str>> {
118	#[serde(rename = "id")]
119	pub tn_id: TnId,
120	pub id_tag: S,
121	pub name: S,
122	#[serde(rename = "type")]
123	pub typ: ProfileType,
124	pub profile_pic: Option<S>,
125	pub cover_pic: Option<S>,
126	#[serde(serialize_with = "serialize_timestamp_iso")]
127	pub created_at: Timestamp,
128	pub x: HashMap<S, S>,
129}
130
131/// Options for listing tenants in meta adapter
132#[derive(Debug, Default)]
133pub struct ListTenantsMetaOptions {
134	pub limit: Option<u32>,
135	pub offset: Option<u32>,
136}
137
138/// Tenant list item from meta adapter (without cover_pic and x fields)
139#[skip_serializing_none]
140#[derive(Debug, Clone, Serialize)]
141#[serde(rename_all = "camelCase")]
142pub struct TenantListMeta {
143	pub tn_id: TnId,
144	pub id_tag: Box<str>,
145	pub name: Box<str>,
146	#[serde(rename = "type")]
147	pub typ: ProfileType,
148	pub profile_pic: Option<Box<str>>,
149	#[serde(serialize_with = "serialize_timestamp_iso")]
150	pub created_at: Timestamp,
151}
152
153#[derive(Debug, Default, Deserialize)]
154pub struct UpdateTenantData {
155	#[serde(rename = "idTag", default)]
156	pub id_tag: Patch<String>,
157	#[serde(default)]
158	pub name: Patch<String>,
159	#[serde(rename = "type", default)]
160	pub typ: Patch<ProfileType>,
161	#[serde(rename = "profilePic", default)]
162	pub profile_pic: Patch<String>,
163	#[serde(rename = "coverPic", default)]
164	pub cover_pic: Patch<String>,
165}
166
167#[derive(Debug)]
168pub struct Profile<S: AsRef<str>> {
169	pub id_tag: S,
170	pub name: S,
171	pub typ: ProfileType,
172	pub profile_pic: Option<S>,
173	pub following: bool,
174	pub connected: ProfileConnectionStatus,
175	pub roles: Option<Box<[Box<str>]>>,
176}
177
178#[derive(Debug, Default, Deserialize)]
179pub struct ListProfileOptions {
180	#[serde(rename = "type")]
181	pub typ: Option<ProfileType>,
182	pub status: Option<Box<[ProfileStatus]>>,
183	pub connected: Option<ProfileConnectionStatus>,
184	pub following: Option<bool>,
185	pub q: Option<String>,
186	pub id_tag: Option<String>,
187}
188
189/// Profile data returned from adapter queries
190#[derive(Debug, Clone, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct ProfileData {
193	pub id_tag: Box<str>,
194	pub name: Box<str>,
195	#[serde(rename = "type")]
196	pub r#type: Box<str>, // "person" or "community"
197	pub profile_pic: Option<Box<str>>,
198	#[serde(serialize_with = "serialize_timestamp_iso")]
199	pub created_at: Timestamp,
200}
201
202/// List of profiles response
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ProfileList {
205	pub profiles: Vec<ProfileData>,
206	pub total: usize,
207	pub limit: usize,
208	pub offset: usize,
209}
210
211#[derive(Debug, Default, Deserialize)]
212pub struct UpdateProfileData {
213	// Profile content fields
214	#[serde(default)]
215	pub name: Patch<Box<str>>,
216	#[serde(default, rename = "profilePic")]
217	pub profile_pic: Patch<Option<Box<str>>>,
218	#[serde(default)]
219	pub roles: Patch<Option<Vec<Box<str>>>>,
220
221	// Status and moderation
222	#[serde(default)]
223	pub status: Patch<ProfileStatus>,
224
225	// Relationship fields
226	#[serde(default)]
227	pub synced: Patch<bool>,
228	#[serde(default)]
229	pub following: Patch<bool>,
230	#[serde(default)]
231	pub connected: Patch<ProfileConnectionStatus>,
232
233	// Sync metadata
234	#[serde(default)]
235	pub etag: Patch<Box<str>>,
236}
237
238// Actions
239//*********
240
241/// Additional action data (cached counts/stats)
242#[derive(Debug, Clone)]
243pub struct ActionData {
244	pub subject: Option<Box<str>>,
245	pub reactions: Option<Box<str>>,
246	pub comments: Option<u32>,
247}
248
249/// Options for updating action metadata
250#[derive(Debug, Clone, Default)]
251pub struct UpdateActionDataOptions {
252	pub subject: Patch<String>,
253	pub reactions: Patch<String>,
254	pub comments: Patch<u32>,
255	pub comments_read: Patch<u32>,
256	pub status: Patch<char>,
257	pub visibility: Patch<char>,
258	pub x: Patch<serde_json::Value>, // Extensible metadata (x.role for SUBS, etc.)
259	pub content: Patch<String>,
260	pub attachments: Patch<String>, // Comma-separated list of attachment IDs
261	pub flags: Patch<String>,
262	pub created_at: Patch<Timestamp>,
263}
264
265/// Options for finalizing an action (resolved fields from ActionCreatorTask)
266#[derive(Debug, Clone, Default)]
267pub struct FinalizeActionOptions<'a> {
268	pub attachments: Option<&'a [&'a str]>,
269	pub subject: Option<&'a str>,
270	pub audience_tag: Option<&'a str>,
271	pub key: Option<&'a str>,
272}
273
274fn deserialize_split<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
275where
276	D: serde::Deserializer<'de>,
277{
278	let s = String::deserialize(deserializer)?;
279	let values: Vec<String> =
280		s.split(',').map(|v| v.trim().to_string()).filter(|v| !v.is_empty()).collect();
281	if values.is_empty() {
282		Ok(None)
283	} else {
284		Ok(Some(values))
285	}
286}
287
288/// Options for listing actions
289#[derive(Debug, Default, Deserialize)]
290#[serde(deny_unknown_fields)]
291pub struct ListActionOptions {
292	/// Maximum number of items to return (default: 20)
293	pub limit: Option<u32>,
294	/// Cursor for pagination (opaque base64-encoded string)
295	pub cursor: Option<String>,
296	/// Sort order: 'created' (default, created_at DESC)
297	pub sort: Option<String>,
298	/// Sort direction: 'asc' or 'desc' (default: desc)
299	#[serde(rename = "sortDir")]
300	pub sort_dir: Option<String>,
301	#[serde(default, rename = "type", deserialize_with = "deserialize_split")]
302	pub typ: Option<Vec<String>>,
303	#[serde(default, deserialize_with = "deserialize_split")]
304	pub status: Option<Vec<String>>,
305	pub tag: Option<String>,
306	pub search: Option<String>,
307	pub visibility: Option<char>,
308	pub issuer: Option<String>,
309	pub audience: Option<String>,
310	pub involved: Option<String>,
311	/// The authenticated user's id_tag (set by handler, not from query params)
312	#[serde(skip)]
313	pub viewer_id_tag: Option<String>,
314	#[serde(rename = "actionId")]
315	pub action_id: Option<String>,
316	#[serde(rename = "parentId")]
317	pub parent_id: Option<String>,
318	#[serde(rename = "rootId")]
319	pub root_id: Option<String>,
320	pub subject: Option<String>,
321	#[serde(rename = "createdAfter")]
322	pub created_after: Option<Timestamp>,
323}
324
325#[skip_serializing_none]
326#[derive(Debug, Clone, Serialize)]
327pub struct ProfileInfo {
328	#[serde(rename = "idTag")]
329	pub id_tag: Box<str>,
330	pub name: Box<str>,
331	#[serde(rename = "type")]
332	pub typ: ProfileType,
333	#[serde(rename = "profilePic")]
334	pub profile_pic: Option<Box<str>>,
335}
336
337pub struct Action<S: AsRef<str>> {
338	pub action_id: S,
339	pub typ: S,
340	pub sub_typ: Option<S>,
341	pub issuer_tag: S,
342	pub parent_id: Option<S>,
343	pub root_id: Option<S>,
344	pub audience_tag: Option<S>,
345	pub content: Option<S>,
346	pub attachments: Option<Vec<S>>,
347	pub subject: Option<S>,
348	pub created_at: Timestamp,
349	pub expires_at: Option<Timestamp>,
350	pub visibility: Option<char>, // None: Direct, P: Public, V: Verified, 2: 2nd degree, F: Follower, C: Connected
351	pub flags: Option<S>,         // Action flags: R/r (reactions), C/c (comments), O/o (open)
352	pub x: Option<serde_json::Value>, // Extensible metadata (x.role for SUBS, etc.)
353}
354
355#[skip_serializing_none]
356#[derive(Debug, Clone, Serialize)]
357pub struct AttachmentView {
358	#[serde(rename = "fileId")]
359	pub file_id: Box<str>,
360	pub dim: Option<(u32, u32)>,
361	#[serde(rename = "localVariants")]
362	pub local_variants: Option<Vec<Box<str>>>,
363}
364
365#[skip_serializing_none]
366#[derive(Debug, Clone, Serialize)]
367#[serde(rename_all = "camelCase")]
368pub struct ActionView {
369	pub action_id: Box<str>,
370	#[serde(rename = "type")]
371	pub typ: Box<str>,
372	#[serde(rename = "subType")]
373	pub sub_typ: Option<Box<str>>,
374	pub parent_id: Option<Box<str>>,
375	pub root_id: Option<Box<str>>,
376	pub issuer: ProfileInfo,
377	pub audience: Option<ProfileInfo>,
378	pub content: Option<serde_json::Value>,
379	pub attachments: Option<Vec<AttachmentView>>,
380	pub subject: Option<Box<str>>,
381	#[serde(serialize_with = "serialize_timestamp_iso")]
382	pub created_at: Timestamp,
383	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
384	pub expires_at: Option<Timestamp>,
385	pub status: Option<Box<str>>,
386	pub stat: Option<serde_json::Value>,
387	pub visibility: Option<char>,
388	pub flags: Option<Box<str>>, // Action flags: R/r (reactions), C/c (comments), O/o (open)
389	pub x: Option<serde_json::Value>, // Extensible metadata (x.role for SUBS, etc.)
390}
391
392// Files
393//*******
394#[derive(Debug)]
395pub enum FileId<S: AsRef<str>> {
396	FileId(S),
397	FId(u64),
398}
399
400pub enum ActionId<S: AsRef<str>> {
401	ActionId(S),
402	AId(u64),
403}
404
405/// File status enum
406/// Note: Mutability is determined by fileTp (BLOB=immutable, CRDT/RTDB=mutable)
407#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
408pub enum FileStatus {
409	#[serde(rename = "A")]
410	Active,
411	#[serde(rename = "P")]
412	Pending,
413	#[serde(rename = "D")]
414	Deleted,
415}
416
417/// User-specific file metadata (access tracking, pinned/starred status)
418#[skip_serializing_none]
419#[derive(Debug, Clone, Default, Serialize)]
420#[serde(rename_all = "camelCase")]
421pub struct FileUserData {
422	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
423	pub accessed_at: Option<Timestamp>,
424	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
425	pub modified_at: Option<Timestamp>,
426	pub pinned: bool,
427	pub starred: bool,
428}
429
430#[skip_serializing_none]
431#[derive(Debug, Clone, Serialize)]
432#[serde(rename_all = "camelCase")]
433pub struct FileView {
434	pub file_id: Box<str>,
435	pub parent_id: Option<Box<str>>, // Parent folder file_id (None = root)
436	pub root_id: Option<Box<str>>,   // Document tree root file_id (None = standalone)
437	pub owner: Option<ProfileInfo>,
438	pub creator: Option<ProfileInfo>,
439	pub preset: Option<Box<str>>,
440	pub content_type: Option<Box<str>>,
441	pub file_name: Box<str>,
442	pub file_tp: Option<Box<str>>, // 'BLOB', 'CRDT', 'RTDB', 'FLDR'
443	#[serde(serialize_with = "serialize_timestamp_iso")]
444	pub created_at: Timestamp,
445	#[serde(serialize_with = "crate::types::serialize_timestamp_iso_opt")]
446	pub accessed_at: Option<Timestamp>, // Global: when anyone last accessed
447	#[serde(serialize_with = "crate::types::serialize_timestamp_iso_opt")]
448	pub modified_at: Option<Timestamp>, // Global: when anyone last modified
449	pub status: FileStatus,
450	pub tags: Option<Vec<Box<str>>>,
451	pub visibility: Option<char>, // None: Direct, P: Public, V: Verified, 2: 2nd degree, F: Follower, C: Connected
452	pub access_level: Option<crate::types::AccessLevel>, // User's access level to this file (R/W)
453	pub user_data: Option<FileUserData>, // User-specific data (only when authenticated)
454	pub x: Option<serde_json::Value>, // Extensible metadata (e.g., {"dim": [width, height]} for images)
455}
456
457#[skip_serializing_none]
458#[derive(Debug, Clone, Serialize)]
459pub struct FileVariant<S: AsRef<str> + Debug> {
460	#[serde(rename = "variantId")]
461	pub variant_id: S,
462	pub variant: S,
463	pub format: S,
464	pub size: u64,
465	pub resolution: (u32, u32),
466	pub available: bool,
467	/// Duration in seconds (for video/audio)
468	pub duration: Option<f64>,
469	/// Bitrate in kbps (for video/audio)
470	pub bitrate: Option<u32>,
471	/// Page count (for documents like PDF)
472	#[serde(rename = "pageCount")]
473	pub page_count: Option<u32>,
474}
475
476impl<S: AsRef<str> + Debug> PartialEq for FileVariant<S> {
477	fn eq(&self, other: &Self) -> bool {
478		self.variant_id.as_ref() == other.variant_id.as_ref()
479			&& self.variant.as_ref() == other.variant.as_ref()
480			&& self.format.as_ref() == other.format.as_ref()
481			&& self.size == other.size
482			&& self.resolution == other.resolution
483			&& self.available == other.available
484			&& self.duration == other.duration
485			&& self.bitrate == other.bitrate
486			&& self.page_count == other.page_count
487	}
488}
489
490impl<S: AsRef<str> + Debug> Eq for FileVariant<S> {}
491
492impl<S: AsRef<str> + Debug + Ord> PartialOrd for FileVariant<S> {
493	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
494		Some(self.cmp(other))
495	}
496}
497
498impl<S: AsRef<str> + Debug + Ord> Ord for FileVariant<S> {
499	fn cmp(&self, other: &Self) -> Ordering {
500		//info!("cmp: {:?} vs {:?}", self, other);
501		self.size
502			.cmp(&other.size)
503			.then_with(|| self.resolution.0.cmp(&other.resolution.0))
504			.then_with(|| self.resolution.1.cmp(&other.resolution.1))
505			.then_with(|| self.size.cmp(&other.size))
506	}
507}
508
509/// Options for listing files
510///
511/// By default (when `status` is `None`), deleted files (status 'D') are excluded.
512/// To include deleted files, explicitly set `status` to `FileStatus::Deleted`.
513#[derive(Debug, Default, Deserialize)]
514#[serde(deny_unknown_fields)]
515pub struct ListFileOptions {
516	/// Maximum number of items to return (default: 30)
517	pub limit: Option<u32>,
518	/// Cursor for pagination (opaque base64-encoded string)
519	pub cursor: Option<String>,
520	#[serde(default, rename = "fileId", deserialize_with = "deserialize_split")]
521	pub file_id: Option<Vec<String>>,
522	#[serde(rename = "parentId")]
523	pub parent_id: Option<String>, // Filter by parent folder (None = root, "__trash__" = trash)
524	#[serde(rename = "rootId")]
525	pub root_id: Option<String>, // Filter by document tree root
526	pub tag: Option<String>,
527	pub preset: Option<String>,
528	pub variant: Option<String>,
529	/// File status filter. If None, excludes deleted files by default.
530	pub status: Option<FileStatus>,
531	#[serde(default, rename = "fileTp", deserialize_with = "deserialize_split")]
532	pub file_type: Option<Vec<String>>,
533	/// Filter by content type pattern (e.g., "image/*", "video/*")
534	#[serde(default, rename = "contentType", deserialize_with = "deserialize_split")]
535	pub content_type: Option<Vec<String>>,
536	/// Substring search in file name
537	#[serde(rename = "fileName")]
538	pub file_name: Option<String>,
539	/// Filter by owner id_tag
540	#[serde(rename = "ownerIdTag")]
541	pub owner_id_tag: Option<String>,
542	/// Exclude files by this owner id_tag
543	#[serde(rename = "notOwnerIdTag")]
544	pub not_owner_id_tag: Option<String>,
545	/// Filter by pinned status (user-specific)
546	pub pinned: Option<bool>,
547	/// Filter by starred status (user-specific)
548	pub starred: Option<bool>,
549	/// Sort order: 'recent' (accessed_at), 'modified' (modified_at), 'name', 'created'
550	pub sort: Option<String>,
551	/// Sort direction: 'asc' or 'desc' (default: desc for dates, asc for name)
552	#[serde(rename = "sortDir")]
553	pub sort_dir: Option<String>,
554	/// User id_tag for user-specific data (set by handler, not from query)
555	#[serde(skip)]
556	pub user_id_tag: Option<String>,
557	/// Scope file_id filter: returns files matching this file_id OR having this root_id.
558	/// Overrides the normal root_id IS NULL constraint. Set by handler for scoped tokens.
559	#[serde(skip)]
560	pub scope_file_id: Option<String>,
561	/// Allowed visibility levels for SQL-level filtering (correct pagination).
562	/// None = no filter (owner sees all including NULL/Direct).
563	/// Set by handler based on subject's access level via `SubjectAccessLevel::visible_levels()`.
564	#[serde(skip)]
565	pub visible_levels: Option<Vec<char>>,
566}
567
568#[derive(Debug, Clone, Default)]
569pub struct CreateFile {
570	pub orig_variant_id: Option<Box<str>>,
571	pub file_id: Option<Box<str>>,
572	pub parent_id: Option<Box<str>>, // Parent folder file_id (None = root)
573	pub root_id: Option<Box<str>>,   // Document tree root file_id (None = standalone)
574	pub owner_tag: Option<Box<str>>, // Set only for files owned by someone OTHER than the tenant (e.g., shared files)
575	pub creator_tag: Option<Box<str>>, // The user who actually created the file
576	pub preset: Option<Box<str>>,
577	pub content_type: Box<str>,
578	pub file_name: Box<str>,
579	pub file_tp: Option<Box<str>>, // 'BLOB', 'CRDT', 'RTDB', 'FLDR' - defaults to 'BLOB'
580	pub created_at: Option<Timestamp>,
581	pub tags: Option<Vec<Box<str>>>,
582	pub x: Option<serde_json::Value>,
583	pub visibility: Option<char>, // None: Direct (default), P: Public, V: Verified, 2: 2nd degree, F: Follower, C: Connected
584	pub status: Option<FileStatus>, // None defaults to Pending, can set to Active for shared files
585}
586
587#[derive(Debug, Clone, Deserialize)]
588pub struct CreateFileVariant {
589	pub variant: Box<str>,
590	pub format: Box<str>,
591	pub resolution: (u32, u32),
592	pub size: u64,
593	pub available: bool,
594}
595
596/// Options for updating file metadata
597#[derive(Debug, Clone, Default, Deserialize)]
598pub struct UpdateFileOptions {
599	#[serde(default, rename = "fileName")]
600	pub file_name: Patch<String>,
601	#[serde(default, rename = "parentId")]
602	pub parent_id: Patch<String>, // Move file to different folder (null = root)
603	#[serde(default)]
604	pub visibility: Patch<char>,
605	#[serde(default)]
606	pub status: Patch<char>,
607}
608
609// Share Entries
610//**************
611
612#[skip_serializing_none]
613#[derive(Debug, Clone, Serialize)]
614#[serde(rename_all = "camelCase")]
615pub struct ShareEntry {
616	pub id: i64,
617	pub resource_type: char,
618	pub resource_id: Box<str>,
619	pub subject_type: char,
620	pub subject_id: Box<str>,
621	pub permission: char,
622	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
623	pub expires_at: Option<Timestamp>,
624	pub created_by: Box<str>,
625	#[serde(serialize_with = "serialize_timestamp_iso")]
626	pub created_at: Timestamp,
627	// Enrichment fields (populated by JOINs in list_by_resource)
628	pub subject_file_name: Option<Box<str>>,
629	pub subject_content_type: Option<Box<str>>,
630	pub subject_file_tp: Option<Box<str>>,
631}
632
633#[derive(Debug, Deserialize)]
634#[serde(rename_all = "camelCase")]
635pub struct CreateShareEntry {
636	pub subject_type: char,
637	pub subject_id: String,
638	pub permission: char,
639	pub expires_at: Option<Timestamp>,
640}
641
642// Push Subscriptions
643//********************
644
645/// Web Push subscription data (RFC 8030)
646#[skip_serializing_none]
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct PushSubscriptionData {
649	/// Push endpoint URL
650	pub endpoint: String,
651	/// Expiration time (Unix timestamp, if provided by browser)
652	#[serde(rename = "expirationTime")]
653	pub expiration_time: Option<i64>,
654	/// Subscription keys (p256dh and auth)
655	pub keys: PushSubscriptionKeys,
656}
657
658/// Subscription keys for Web Push encryption
659#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct PushSubscriptionKeys {
661	/// P-256 public key for encryption (base64url encoded)
662	pub p256dh: String,
663	/// Authentication secret (base64url encoded)
664	pub auth: String,
665}
666
667/// Full push subscription record stored in database
668#[derive(Debug, Clone, Serialize)]
669#[serde(rename_all = "camelCase")]
670pub struct PushSubscription {
671	/// Unique subscription ID
672	pub id: u64,
673	/// The subscription data (endpoint, keys, etc.)
674	pub subscription: PushSubscriptionData,
675	/// When this subscription was created
676	#[serde(serialize_with = "serialize_timestamp_iso")]
677	pub created_at: Timestamp,
678}
679
680// Tasks
681//*******
682pub struct Task {
683	pub task_id: u64,
684	pub tn_id: TnId,
685	pub kind: Box<str>,
686	pub status: char,
687	pub created_at: Timestamp,
688	pub next_at: Option<Timestamp>,
689	pub input: Box<str>,
690	pub output: Box<str>,
691	pub deps: Box<[u64]>,
692	pub retry: Option<Box<str>>,
693	pub cron: Option<Box<str>>,
694}
695
696#[derive(Debug, Default)]
697pub struct TaskPatch {
698	pub input: Patch<String>,
699	pub next_at: Patch<Timestamp>,
700	pub deps: Patch<Vec<u64>>,
701	pub retry: Patch<String>,
702	pub cron: Patch<String>,
703}
704
705#[derive(Debug, Default)]
706pub struct ListTaskOptions {}
707
708// Installed Apps
709//***************
710
711/// Data for installing an app
712#[derive(Debug)]
713pub struct InstallApp {
714	pub app_name: Box<str>,
715	pub publisher_tag: Box<str>,
716	pub version: Box<str>,
717	pub action_id: Box<str>,
718	pub file_id: Box<str>,
719	pub blob_id: Box<str>,
720	pub capabilities: Option<Vec<Box<str>>>,
721}
722
723/// Installed app record
724#[derive(Debug, Serialize)]
725#[serde(rename_all = "camelCase")]
726pub struct InstalledApp {
727	pub app_name: Box<str>,
728	pub publisher_tag: Box<str>,
729	pub version: Box<str>,
730	pub action_id: Box<str>,
731	pub file_id: Box<str>,
732	pub blob_id: Box<str>,
733	pub status: Box<str>,
734	pub capabilities: Option<Vec<Box<str>>>,
735	pub auto_update: bool,
736	#[serde(serialize_with = "serialize_timestamp_iso")]
737	pub installed_at: Timestamp,
738}
739
740#[async_trait]
741pub trait MetaAdapter: Debug + Send + Sync {
742	// Tenant management
743	//*******************
744
745	/// Reads a tenant profile
746	async fn read_tenant(&self, tn_id: TnId) -> ClResult<Tenant<Box<str>>>;
747
748	/// Creates a new tenant
749	async fn create_tenant(&self, tn_id: TnId, id_tag: &str) -> ClResult<TnId>;
750
751	/// Updates a tenant
752	async fn update_tenant(&self, tn_id: TnId, tenant: &UpdateTenantData) -> ClResult<()>;
753
754	/// Deletes a tenant
755	async fn delete_tenant(&self, tn_id: TnId) -> ClResult<()>;
756
757	/// Lists all tenants (for admin use)
758	async fn list_tenants(&self, opts: &ListTenantsMetaOptions) -> ClResult<Vec<TenantListMeta>>;
759
760	/// Lists all profiles matching a set of options
761	async fn list_profiles(
762		&self,
763		tn_id: TnId,
764		opts: &ListProfileOptions,
765	) -> ClResult<Vec<Profile<Box<str>>>>;
766
767	/// Get relationships between the current user and multiple target profiles
768	///
769	/// Efficiently queries relationship status (following, connected) for multiple profiles
770	/// in a single database call, avoiding N+1 query patterns.
771	///
772	/// Returns: HashMap<target_id_tag, (following: bool, connected: bool)>
773	async fn get_relationships(
774		&self,
775		tn_id: TnId,
776		target_id_tags: &[&str],
777	) -> ClResult<HashMap<String, (bool, bool)>>;
778
779	/// Reads a profile
780	///
781	/// Returns an `(etag, Profile)` tuple.
782	async fn read_profile(
783		&self,
784		tn_id: TnId,
785		id_tag: &str,
786	) -> ClResult<(Box<str>, Profile<Box<str>>)>;
787
788	/// Read profile roles for access token generation
789	async fn read_profile_roles(
790		&self,
791		tn_id: TnId,
792		id_tag: &str,
793	) -> ClResult<Option<Box<[Box<str>]>>>;
794
795	async fn create_profile(
796		&self,
797		tn_id: TnId,
798		profile: &Profile<&str>,
799		etag: &str,
800	) -> ClResult<()>;
801	async fn update_profile(
802		&self,
803		tn_id: TnId,
804		id_tag: &str,
805		profile: &UpdateProfileData,
806	) -> ClResult<()>;
807
808	/// Reads the public key of a profile
809	///
810	/// Returns a `(public key, expiration)` tuple.
811	async fn read_profile_public_key(
812		&self,
813		id_tag: &str,
814		key_id: &str,
815	) -> ClResult<(Box<str>, Timestamp)>;
816	async fn add_profile_public_key(
817		&self,
818		id_tag: &str,
819		key_id: &str,
820		public_key: &str,
821	) -> ClResult<()>;
822	/// Process profile refresh
823	/// callback(tn_id: TnId, id_tag: &str, etag: Option<&str>)
824	//async fn process_profile_refresh(&self, callback: FnOnce<(TnId, &str, Option<&str>)>);
825	//async fn process_profile_refresh<'a, F>(&self, callback: F)
826	//	where F: FnOnce(TnId, &'a str, Option<&'a str>) -> ClResult<()> + Send;
827	async fn process_profile_refresh<'a>(
828		&self,
829		callback: Box<dyn Fn(TnId, &'a str, Option<&'a str>) -> ClResult<()> + Send>,
830	);
831
832	/// List stale profiles that need refreshing
833	///
834	/// Returns profiles where `synced_at IS NULL OR synced_at < now - max_age_secs`.
835	/// Returns `Vec<(tn_id, id_tag, etag)>` tuples for conditional refresh requests.
836	async fn list_stale_profiles(
837		&self,
838		max_age_secs: i64,
839		limit: u32,
840	) -> ClResult<Vec<(TnId, Box<str>, Option<Box<str>>)>>;
841
842	// Action management
843	//*******************
844	async fn get_action_id(&self, tn_id: TnId, a_id: u64) -> ClResult<Box<str>>;
845	async fn list_actions(
846		&self,
847		tn_id: TnId,
848		opts: &ListActionOptions,
849	) -> ClResult<Vec<ActionView>>;
850	async fn list_action_tokens(
851		&self,
852		tn_id: TnId,
853		opts: &ListActionOptions,
854	) -> ClResult<Box<[Box<str>]>>;
855
856	async fn create_action(
857		&self,
858		tn_id: TnId,
859		action: &Action<&str>,
860		key: Option<&str>,
861	) -> ClResult<ActionId<Box<str>>>;
862
863	async fn finalize_action(
864		&self,
865		tn_id: TnId,
866		a_id: u64,
867		action_id: &str,
868		options: FinalizeActionOptions<'_>,
869	) -> ClResult<()>;
870
871	async fn create_inbound_action(
872		&self,
873		tn_id: TnId,
874		action_id: &str,
875		token: &str,
876		ack_token: Option<&str>,
877	) -> ClResult<()>;
878
879	/// Get the root_id of an action
880	async fn get_action_root_id(&self, tn_id: TnId, action_id: &str) -> ClResult<Box<str>>;
881
882	/// Get action data (subject, reaction count, comment count)
883	async fn get_action_data(&self, tn_id: TnId, action_id: &str) -> ClResult<Option<ActionData>>;
884
885	/// Get action by key
886	async fn get_action_by_key(
887		&self,
888		tn_id: TnId,
889		action_key: &str,
890	) -> ClResult<Option<Action<Box<str>>>>;
891
892	/// Store action token for federation (called when action is created)
893	async fn store_action_token(&self, tn_id: TnId, action_id: &str, token: &str) -> ClResult<()>;
894
895	/// Get action token for federation
896	async fn get_action_token(&self, tn_id: TnId, action_id: &str) -> ClResult<Option<Box<str>>>;
897
898	/// Update action data (subject, reactions, comments, status)
899	async fn update_action_data(
900		&self,
901		tn_id: TnId,
902		action_id: &str,
903		opts: &UpdateActionDataOptions,
904	) -> ClResult<()>;
905
906	/// Update inbound action status
907	async fn update_inbound_action(
908		&self,
909		tn_id: TnId,
910		action_id: &str,
911		status: Option<char>,
912	) -> ClResult<()>;
913
914	/// Get related action tokens by APRV action_id
915	/// Returns list of (action_id, token) pairs for actions that have ack = aprv_action_id
916	async fn get_related_action_tokens(
917		&self,
918		tn_id: TnId,
919		aprv_action_id: &str,
920	) -> ClResult<Vec<(Box<str>, Box<str>)>>;
921
922	// File management
923	//*****************
924	async fn get_file_id(&self, tn_id: TnId, f_id: u64) -> ClResult<Box<str>>;
925	async fn list_files(&self, tn_id: TnId, opts: &ListFileOptions) -> ClResult<Vec<FileView>>;
926	async fn list_file_variants(
927		&self,
928		tn_id: TnId,
929		file_id: FileId<&str>,
930	) -> ClResult<Vec<FileVariant<Box<str>>>>;
931	/// List locally available variant names for a file (only those marked available)
932	async fn list_available_variants(&self, tn_id: TnId, file_id: &str) -> ClResult<Vec<Box<str>>>;
933	async fn read_file_variant(
934		&self,
935		tn_id: TnId,
936		variant_id: &str,
937	) -> ClResult<FileVariant<Box<str>>>;
938	/// Look up the file_id for a given variant_id
939	async fn read_file_id_by_variant(&self, tn_id: TnId, variant_id: &str) -> ClResult<Box<str>>;
940	/// Look up the internal f_id for a given file_id (for adding variants to existing files)
941	async fn read_f_id_by_file_id(&self, tn_id: TnId, file_id: &str) -> ClResult<u64>;
942	async fn create_file(&self, tn_id: TnId, opts: CreateFile) -> ClResult<FileId<Box<str>>>;
943	async fn create_file_variant<'a>(
944		&'a self,
945		tn_id: TnId,
946		f_id: u64,
947		opts: FileVariant<&'a str>,
948	) -> ClResult<&'a str>;
949	async fn update_file_id(&self, tn_id: TnId, f_id: u64, file_id: &str) -> ClResult<()>;
950
951	/// Finalize a pending file - sets file_id and transitions status from 'P' to 'A' atomically
952	async fn finalize_file(&self, tn_id: TnId, f_id: u64, file_id: &str) -> ClResult<()>;
953
954	// Task scheduler
955	//****************
956	async fn list_tasks(&self, opts: ListTaskOptions) -> ClResult<Vec<Task>>;
957	async fn list_task_ids(&self, kind: &str, keys: &[Box<str>]) -> ClResult<Vec<u64>>;
958	async fn create_task(
959		&self,
960		kind: &'static str,
961		key: Option<&str>,
962		input: &str,
963		deps: &[u64],
964	) -> ClResult<u64>;
965	async fn update_task_finished(&self, task_id: u64, output: &str) -> ClResult<()>;
966	async fn update_task_error(
967		&self,
968		task_id: u64,
969		output: &str,
970		next_at: Option<Timestamp>,
971	) -> ClResult<()>;
972
973	/// Find a pending task by its key
974	async fn find_task_by_key(&self, key: &str) -> ClResult<Option<Task>>;
975
976	/// Update task fields with partial updates
977	async fn update_task(&self, task_id: u64, patch: &TaskPatch) -> ClResult<()>;
978
979	// Phase 1: Profile Management
980	//****************************
981	/// Get a single profile by id_tag
982	async fn get_profile_info(&self, tn_id: TnId, id_tag: &str) -> ClResult<ProfileData>;
983
984	// Phase 2: Action Management
985	//***************************
986	/// Get a single action by action_id
987	async fn get_action(&self, tn_id: TnId, action_id: &str) -> ClResult<Option<ActionView>>;
988
989	/// Update action content and attachments (if not yet federated)
990	async fn update_action(
991		&self,
992		tn_id: TnId,
993		action_id: &str,
994		content: Option<&str>,
995		attachments: Option<&[&str]>,
996	) -> ClResult<()>;
997
998	/// Delete an action (soft delete with cleanup)
999	async fn delete_action(&self, tn_id: TnId, action_id: &str) -> ClResult<()>;
1000
1001	/// Count active (non-DEL, non-deleted) REACT actions for a given subject, grouped by type
1002	/// Returns colon-separated format: "L5:V3:W1" (Like=5, Love=3, Wow=1)
1003	async fn count_reactions(&self, tn_id: TnId, subject_id: &str) -> ClResult<String>;
1004
1005	// Phase 2: File Management Enhancements
1006	//**************************************
1007	/// Delete a file (set status to 'D')
1008	async fn delete_file(&self, tn_id: TnId, file_id: &str) -> ClResult<()>;
1009
1010	/// List all child files in a document tree (files with the given root_id)
1011	async fn list_children_by_root(&self, tn_id: TnId, root_id: &str) -> ClResult<Vec<Box<str>>>;
1012
1013	// Settings Management
1014	//*********************
1015	/// List all settings for a tenant, optionally filtered by prefix
1016	async fn list_settings(
1017		&self,
1018		tn_id: TnId,
1019		prefix: Option<&[String]>,
1020	) -> ClResult<std::collections::HashMap<String, serde_json::Value>>;
1021
1022	/// Read a single setting by name
1023	async fn read_setting(&self, tn_id: TnId, name: &str) -> ClResult<Option<serde_json::Value>>;
1024
1025	/// Update or delete a setting (None = delete)
1026	async fn update_setting(
1027		&self,
1028		tn_id: TnId,
1029		name: &str,
1030		value: Option<serde_json::Value>,
1031	) -> ClResult<()>;
1032
1033	// Reference / Bookmark Management
1034	//********************************
1035	/// List all references for a tenant
1036	async fn list_refs(&self, tn_id: TnId, opts: &ListRefsOptions) -> ClResult<Vec<RefData>>;
1037
1038	/// Get a specific reference by ID
1039	async fn get_ref(&self, tn_id: TnId, ref_id: &str) -> ClResult<Option<(Box<str>, Box<str>)>>;
1040
1041	/// Create a new reference
1042	async fn create_ref(
1043		&self,
1044		tn_id: TnId,
1045		ref_id: &str,
1046		opts: &CreateRefOptions,
1047	) -> ClResult<RefData>;
1048
1049	/// Delete a reference
1050	async fn delete_ref(&self, tn_id: TnId, ref_id: &str) -> ClResult<()>;
1051
1052	/// Use/consume a reference - validates type, expiration, counter, decrements counter
1053	/// Returns (TnId, id_tag, RefData) of the tenant that owns this ref
1054	async fn use_ref(
1055		&self,
1056		ref_id: &str,
1057		expected_types: &[&str],
1058	) -> ClResult<(TnId, Box<str>, RefData)>;
1059
1060	/// Validate a reference without consuming it - checks type, expiration, counter
1061	/// Returns (TnId, id_tag, RefData) of the tenant that owns this ref if valid
1062	async fn validate_ref(
1063		&self,
1064		ref_id: &str,
1065		expected_types: &[&str],
1066	) -> ClResult<(TnId, Box<str>, RefData)>;
1067
1068	// Tag Management
1069	//***************
1070	/// List all tags for a tenant
1071	///
1072	/// # Arguments
1073	/// * `tn_id` - Tenant ID
1074	/// * `prefix` - Optional prefix filter
1075	/// * `with_counts` - If true, include file counts per tag
1076	/// * `limit` - Optional limit on number of tags returned
1077	async fn list_tags(
1078		&self,
1079		tn_id: TnId,
1080		prefix: Option<&str>,
1081		with_counts: bool,
1082		limit: Option<u32>,
1083	) -> ClResult<Vec<TagInfo>>;
1084
1085	/// Add a tag to a file
1086	async fn add_tag(&self, tn_id: TnId, file_id: &str, tag: &str) -> ClResult<Vec<String>>;
1087
1088	/// Remove a tag from a file
1089	async fn remove_tag(&self, tn_id: TnId, file_id: &str, tag: &str) -> ClResult<Vec<String>>;
1090
1091	// File Management Enhancements
1092	//****************************
1093	/// Update file metadata (name, visibility, status)
1094	async fn update_file_data(
1095		&self,
1096		tn_id: TnId,
1097		file_id: &str,
1098		opts: &UpdateFileOptions,
1099	) -> ClResult<()>;
1100
1101	/// Read file metadata
1102	async fn read_file(&self, tn_id: TnId, file_id: &str) -> ClResult<Option<FileView>>;
1103
1104	// File User Data (per-user file activity tracking)
1105	//**************************************************
1106
1107	/// Record file access for a user (upserts record, updates accessed_at timestamp)
1108	async fn record_file_access(&self, tn_id: TnId, id_tag: &str, file_id: &str) -> ClResult<()>;
1109
1110	/// Record file modification for a user (upserts record, updates modified_at timestamp)
1111	async fn record_file_modification(
1112		&self,
1113		tn_id: TnId,
1114		id_tag: &str,
1115		file_id: &str,
1116	) -> ClResult<()>;
1117
1118	/// Update file user data (pinned/starred status)
1119	async fn update_file_user_data(
1120		&self,
1121		tn_id: TnId,
1122		id_tag: &str,
1123		file_id: &str,
1124		pinned: Option<bool>,
1125		starred: Option<bool>,
1126	) -> ClResult<FileUserData>;
1127
1128	/// Get file user data for a specific file
1129	async fn get_file_user_data(
1130		&self,
1131		tn_id: TnId,
1132		id_tag: &str,
1133		file_id: &str,
1134	) -> ClResult<Option<FileUserData>>;
1135
1136	// Push Subscription Management
1137	//*****************************
1138
1139	/// List all push subscriptions for a tenant (user)
1140	///
1141	/// Returns all active push subscriptions for this tenant.
1142	/// Each tenant represents a user, so this returns all their device subscriptions.
1143	async fn list_push_subscriptions(&self, tn_id: TnId) -> ClResult<Vec<PushSubscription>>;
1144
1145	/// Create a new push subscription
1146	///
1147	/// Stores a Web Push subscription for a tenant. The subscription contains
1148	/// the endpoint URL and encryption keys needed to send push notifications.
1149	/// Returns the generated subscription ID.
1150	async fn create_push_subscription(
1151		&self,
1152		tn_id: TnId,
1153		subscription: &PushSubscriptionData,
1154	) -> ClResult<u64>;
1155
1156	/// Delete a push subscription by ID
1157	///
1158	/// Removes a push subscription. Called when a subscription becomes invalid
1159	/// (e.g., 410 Gone response from push service) or when user unsubscribes.
1160	async fn delete_push_subscription(&self, tn_id: TnId, subscription_id: u64) -> ClResult<()>;
1161
1162	// Share Entry Management
1163	//***********************
1164
1165	/// Create a share entry (idempotent on unique constraint)
1166	async fn create_share_entry(
1167		&self,
1168		tn_id: TnId,
1169		resource_type: char,
1170		resource_id: &str,
1171		created_by: &str,
1172		entry: &CreateShareEntry,
1173	) -> ClResult<ShareEntry>;
1174
1175	/// Delete a share entry by ID
1176	async fn delete_share_entry(&self, tn_id: TnId, id: i64) -> ClResult<()>;
1177
1178	/// List share entries for a resource
1179	async fn list_share_entries(
1180		&self,
1181		tn_id: TnId,
1182		resource_type: char,
1183		resource_id: &str,
1184	) -> ClResult<Vec<ShareEntry>>;
1185
1186	/// List share entries by subject (reverse lookup).
1187	/// If `subject_type` is None, matches all subject types.
1188	async fn list_share_entries_by_subject(
1189		&self,
1190		tn_id: TnId,
1191		subject_type: Option<char>,
1192		subject_id: &str,
1193	) -> ClResult<Vec<ShareEntry>>;
1194
1195	/// Check if a subject has share access to a resource
1196	/// Returns the permission char if access exists, None otherwise
1197	async fn check_share_access(
1198		&self,
1199		tn_id: TnId,
1200		resource_type: char,
1201		resource_id: &str,
1202		subject_type: char,
1203		subject_id: &str,
1204	) -> ClResult<Option<char>>;
1205
1206	/// Read a single share entry by ID (for delete validation)
1207	async fn read_share_entry(&self, tn_id: TnId, id: i64) -> ClResult<Option<ShareEntry>>;
1208
1209	// Installed App Management
1210	//*************************
1211
1212	/// Install an app package
1213	async fn install_app(&self, tn_id: TnId, install: &InstallApp) -> ClResult<()>;
1214
1215	/// Uninstall an app by name and publisher
1216	async fn uninstall_app(&self, tn_id: TnId, app_name: &str, publisher_tag: &str)
1217		-> ClResult<()>;
1218
1219	/// List installed apps, optionally filtered by search term
1220	async fn list_installed_apps(
1221		&self,
1222		tn_id: TnId,
1223		search: Option<&str>,
1224	) -> ClResult<Vec<InstalledApp>>;
1225
1226	/// Get a specific installed app
1227	async fn get_installed_app(
1228		&self,
1229		tn_id: TnId,
1230		app_name: &str,
1231		publisher_tag: &str,
1232	) -> ClResult<Option<InstalledApp>>;
1233}
1234
1235#[cfg(test)]
1236mod tests {
1237	use super::*;
1238	#[test]
1239	fn test_deserialize_list_action_options_with_multiple_statuses() {
1240		let query = "status=C,N&type=POST,REPLY";
1241		let opts: ListActionOptions =
1242			serde_urlencoded::from_str(query).expect("should deserialize");
1243
1244		assert!(opts.status.is_some());
1245		let statuses = opts.status.expect("status should be Some");
1246		assert_eq!(statuses.len(), 2);
1247		assert_eq!(statuses[0].as_str(), "C");
1248		assert_eq!(statuses[1].as_str(), "N");
1249
1250		assert!(opts.typ.is_some());
1251		let types = opts.typ.expect("type should be Some");
1252		assert_eq!(types.len(), 2);
1253		assert_eq!(types[0].as_str(), "POST");
1254		assert_eq!(types[1].as_str(), "REPLY");
1255	}
1256
1257	#[test]
1258	fn test_deserialize_list_action_options_without_status() {
1259		let query = "issuer=alice";
1260		let opts: ListActionOptions =
1261			serde_urlencoded::from_str(query).expect("should deserialize");
1262
1263		assert!(opts.status.is_none());
1264		assert!(opts.typ.is_none());
1265		assert_eq!(opts.issuer.as_deref(), Some("alice"));
1266	}
1267
1268	#[test]
1269	fn test_deserialize_list_action_options_single_status() {
1270		let query = "status=C";
1271		let opts: ListActionOptions =
1272			serde_urlencoded::from_str(query).expect("should deserialize");
1273
1274		assert!(opts.status.is_some());
1275		let statuses = opts.status.expect("status should be Some");
1276		assert_eq!(statuses.len(), 1);
1277		assert_eq!(statuses[0].as_str(), "C");
1278	}
1279}
1280
1281// vim: ts=4