Skip to main content

cloudillo_types/
meta_adapter.rs

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