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, Patch, Timestamp, TnId},
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<u32>,
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<u32>,
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}
252
253/// Options for finalizing an action (resolved fields from ActionCreatorTask)
254#[derive(Debug, Clone, Default)]
255pub struct FinalizeActionOptions<'a> {
256	pub attachments: Option<&'a [&'a str]>,
257	pub subject: Option<&'a str>,
258	pub audience_tag: Option<&'a str>,
259	pub key: Option<&'a str>,
260}
261
262#[derive(Debug, Clone)]
263pub struct CreateOutboundActionOptions {
264	pub recipient_tag: String,
265	pub typ: String,
266}
267
268fn deserialize_split<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
269where
270	D: serde::Deserializer<'de>,
271{
272	let s = String::deserialize(deserializer)?;
273	Ok(Some(s.split(',').map(|v| v.trim().to_string()).collect()))
274}
275
276/// Options for listing actions
277#[derive(Debug, Default, Deserialize)]
278#[serde(deny_unknown_fields)]
279pub struct ListActionOptions {
280	/// Maximum number of items to return (default: 20)
281	pub limit: Option<u32>,
282	/// Cursor for pagination (opaque base64-encoded string)
283	pub cursor: Option<String>,
284	/// Sort order: 'created' (default, created_at DESC)
285	pub sort: Option<String>,
286	/// Sort direction: 'asc' or 'desc' (default: desc)
287	#[serde(rename = "sortDir")]
288	pub sort_dir: Option<String>,
289	#[serde(default, rename = "type", deserialize_with = "deserialize_split")]
290	pub typ: Option<Vec<String>>,
291	#[serde(default, deserialize_with = "deserialize_split")]
292	pub status: Option<Vec<String>>,
293	pub tag: Option<String>,
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/// Reaction data
379#[derive(Debug, Clone, Serialize, Deserialize)]
380#[serde(rename_all = "camelCase")]
381pub struct ReactionData {
382	pub id: Box<str>,
383	pub action_id: Box<str>,
384	pub reactor_id_tag: Box<str>,
385	pub r#type: Box<str>,
386	pub content: Option<Box<str>>,
387	#[serde(serialize_with = "serialize_timestamp_iso")]
388	pub created_at: Timestamp,
389}
390
391// Files
392//*******
393#[derive(Debug)]
394pub enum FileId<S: AsRef<str>> {
395	FileId(S),
396	FId(u64),
397}
398
399pub enum ActionId<S: AsRef<str>> {
400	ActionId(S),
401	AId(u64),
402}
403
404/// File status enum
405/// Note: Mutability is determined by fileTp (BLOB=immutable, CRDT/RTDB=mutable)
406#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
407pub enum FileStatus {
408	#[serde(rename = "A")]
409	Active,
410	#[serde(rename = "P")]
411	Pending,
412	#[serde(rename = "D")]
413	Deleted,
414}
415
416/// User-specific file metadata (access tracking, pinned/starred status)
417#[skip_serializing_none]
418#[derive(Debug, Clone, Default, Serialize)]
419#[serde(rename_all = "camelCase")]
420pub struct FileUserData {
421	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
422	pub accessed_at: Option<Timestamp>,
423	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
424	pub modified_at: Option<Timestamp>,
425	pub pinned: bool,
426	pub starred: bool,
427}
428
429#[skip_serializing_none]
430#[derive(Debug, Clone, Serialize)]
431#[serde(rename_all = "camelCase")]
432pub struct FileView {
433	pub file_id: Box<str>,
434	pub parent_id: Option<Box<str>>, // Parent folder file_id (None = root)
435	pub owner: Option<ProfileInfo>,
436	pub creator: Option<ProfileInfo>,
437	pub preset: Option<Box<str>>,
438	pub content_type: Option<Box<str>>,
439	pub file_name: Box<str>,
440	pub file_tp: Option<Box<str>>, // 'BLOB', 'CRDT', 'RTDB', 'FLDR'
441	#[serde(serialize_with = "serialize_timestamp_iso")]
442	pub created_at: Timestamp,
443	#[serde(serialize_with = "crate::types::serialize_timestamp_iso_opt")]
444	pub accessed_at: Option<Timestamp>, // Global: when anyone last accessed
445	#[serde(serialize_with = "crate::types::serialize_timestamp_iso_opt")]
446	pub modified_at: Option<Timestamp>, // Global: when anyone last modified
447	pub status: FileStatus,
448	pub tags: Option<Vec<Box<str>>>,
449	pub visibility: Option<char>, // None: Direct, P: Public, V: Verified, 2: 2nd degree, F: Follower, C: Connected
450	pub access_level: Option<crate::types::AccessLevel>, // User's access level to this file (R/W)
451	pub user_data: Option<FileUserData>, // User-specific data (only when authenticated)
452	pub x: Option<serde_json::Value>, // Extensible metadata (e.g., {"dim": [width, height]} for images)
453}
454
455#[skip_serializing_none]
456#[derive(Debug, Clone, Serialize)]
457pub struct FileVariant<S: AsRef<str> + Debug> {
458	#[serde(rename = "variantId")]
459	pub variant_id: S,
460	pub variant: S,
461	pub format: S,
462	pub size: u64,
463	pub resolution: (u32, u32),
464	pub available: bool,
465	/// Duration in seconds (for video/audio)
466	pub duration: Option<f64>,
467	/// Bitrate in kbps (for video/audio)
468	pub bitrate: Option<u32>,
469	/// Page count (for documents like PDF)
470	#[serde(rename = "pageCount")]
471	pub page_count: Option<u32>,
472}
473
474impl<S: AsRef<str> + Debug> PartialEq for FileVariant<S> {
475	fn eq(&self, other: &Self) -> bool {
476		self.variant_id.as_ref() == other.variant_id.as_ref()
477			&& self.variant.as_ref() == other.variant.as_ref()
478			&& self.format.as_ref() == other.format.as_ref()
479			&& self.size == other.size
480			&& self.resolution == other.resolution
481			&& self.available == other.available
482			&& self.duration == other.duration
483			&& self.bitrate == other.bitrate
484			&& self.page_count == other.page_count
485	}
486}
487
488impl<S: AsRef<str> + Debug> Eq for FileVariant<S> {}
489
490impl<S: AsRef<str> + Debug + Ord> PartialOrd for FileVariant<S> {
491	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
492		Some(self.cmp(other))
493	}
494}
495
496impl<S: AsRef<str> + Debug + Ord> Ord for FileVariant<S> {
497	fn cmp(&self, other: &Self) -> Ordering {
498		//info!("cmp: {:?} vs {:?}", self, other);
499		self.size
500			.cmp(&other.size)
501			.then_with(|| self.resolution.0.cmp(&other.resolution.0))
502			.then_with(|| self.resolution.1.cmp(&other.resolution.1))
503			.then_with(|| self.size.cmp(&other.size))
504	}
505}
506
507/// Options for listing files
508///
509/// By default (when `status` is `None`), deleted files (status 'D') are excluded.
510/// To include deleted files, explicitly set `status` to `FileStatus::Deleted`.
511#[derive(Debug, Default, Deserialize)]
512#[serde(deny_unknown_fields)]
513pub struct ListFileOptions {
514	/// Maximum number of items to return (default: 30)
515	pub limit: Option<u32>,
516	/// Cursor for pagination (opaque base64-encoded string)
517	pub cursor: Option<String>,
518	#[serde(rename = "fileId")]
519	pub file_id: Option<String>,
520	#[serde(rename = "parentId")]
521	pub parent_id: Option<String>, // Filter by parent folder (None = root, "__trash__" = trash)
522	pub tag: Option<String>,
523	pub preset: Option<String>,
524	pub variant: Option<String>,
525	/// File status filter. If None, excludes deleted files by default.
526	pub status: Option<FileStatus>,
527	#[serde(rename = "fileTp")]
528	pub file_type: Option<String>,
529	/// Filter by content type pattern (e.g., "image/*", "video/*")
530	#[serde(rename = "contentType")]
531	pub content_type: Option<String>,
532	/// Filter by pinned status (user-specific)
533	pub pinned: Option<bool>,
534	/// Filter by starred status (user-specific)
535	pub starred: Option<bool>,
536	/// Sort order: 'recent' (accessed_at), 'modified' (modified_at), 'name', 'created'
537	pub sort: Option<String>,
538	/// Sort direction: 'asc' or 'desc' (default: desc for dates, asc for name)
539	#[serde(rename = "sortDir")]
540	pub sort_dir: Option<String>,
541	/// User id_tag for user-specific data (set by handler, not from query)
542	#[serde(skip)]
543	pub user_id_tag: Option<String>,
544}
545
546#[derive(Debug, Clone, Default)]
547pub struct CreateFile {
548	pub orig_variant_id: Option<Box<str>>,
549	pub file_id: Option<Box<str>>,
550	pub parent_id: Option<Box<str>>, // Parent folder file_id (None = root)
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// Push Subscriptions
587//********************
588
589/// Web Push subscription data (RFC 8030)
590#[skip_serializing_none]
591#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct PushSubscriptionData {
593	/// Push endpoint URL
594	pub endpoint: String,
595	/// Expiration time (Unix timestamp, if provided by browser)
596	#[serde(rename = "expirationTime")]
597	pub expiration_time: Option<i64>,
598	/// Subscription keys (p256dh and auth)
599	pub keys: PushSubscriptionKeys,
600}
601
602/// Subscription keys for Web Push encryption
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct PushSubscriptionKeys {
605	/// P-256 public key for encryption (base64url encoded)
606	pub p256dh: String,
607	/// Authentication secret (base64url encoded)
608	pub auth: String,
609}
610
611/// Full push subscription record stored in database
612#[derive(Debug, Clone, Serialize)]
613#[serde(rename_all = "camelCase")]
614pub struct PushSubscription {
615	/// Unique subscription ID
616	pub id: u64,
617	/// The subscription data (endpoint, keys, etc.)
618	pub subscription: PushSubscriptionData,
619	/// When this subscription was created
620	#[serde(serialize_with = "serialize_timestamp_iso")]
621	pub created_at: Timestamp,
622}
623
624// Tasks
625//*******
626pub struct Task {
627	pub task_id: u64,
628	pub tn_id: TnId,
629	pub kind: Box<str>,
630	pub status: char,
631	pub created_at: Timestamp,
632	pub next_at: Option<Timestamp>,
633	pub input: Box<str>,
634	pub output: Box<str>,
635	pub deps: Box<[u64]>,
636	pub retry: Option<Box<str>>,
637	pub cron: Option<Box<str>>,
638}
639
640#[derive(Debug, Default)]
641pub struct TaskPatch {
642	pub input: Patch<String>,
643	pub next_at: Patch<Timestamp>,
644	pub deps: Patch<Vec<u64>>,
645	pub retry: Patch<String>,
646	pub cron: Patch<String>,
647}
648
649#[derive(Debug, Default)]
650pub struct ListTaskOptions {}
651
652#[async_trait]
653pub trait MetaAdapter: Debug + Send + Sync {
654	// Tenant management
655	//*******************
656
657	/// Reads a tenant profile
658	async fn read_tenant(&self, tn_id: TnId) -> ClResult<Tenant<Box<str>>>;
659
660	/// Creates a new tenant
661	async fn create_tenant(&self, tn_id: TnId, id_tag: &str) -> ClResult<TnId>;
662
663	/// Updates a tenant
664	async fn update_tenant(&self, tn_id: TnId, tenant: &UpdateTenantData) -> ClResult<()>;
665
666	/// Deletes a tenant
667	async fn delete_tenant(&self, tn_id: TnId) -> ClResult<()>;
668
669	/// Lists all tenants (for admin use)
670	async fn list_tenants(&self, opts: &ListTenantsMetaOptions) -> ClResult<Vec<TenantListMeta>>;
671
672	/// Lists all profiles matching a set of options
673	async fn list_profiles(
674		&self,
675		tn_id: TnId,
676		opts: &ListProfileOptions,
677	) -> ClResult<Vec<Profile<Box<str>>>>;
678
679	/// Get relationships between the current user and multiple target profiles
680	///
681	/// Efficiently queries relationship status (following, connected) for multiple profiles
682	/// in a single database call, avoiding N+1 query patterns.
683	///
684	/// Returns: HashMap<target_id_tag, (following: bool, connected: bool)>
685	async fn get_relationships(
686		&self,
687		tn_id: TnId,
688		target_id_tags: &[&str],
689	) -> ClResult<HashMap<String, (bool, bool)>>;
690
691	/// Reads a profile
692	///
693	/// Returns an `(etag, Profile)` tuple.
694	async fn read_profile(
695		&self,
696		tn_id: TnId,
697		id_tag: &str,
698	) -> ClResult<(Box<str>, Profile<Box<str>>)>;
699
700	/// Read profile roles for access token generation
701	async fn read_profile_roles(
702		&self,
703		tn_id: TnId,
704		id_tag: &str,
705	) -> ClResult<Option<Box<[Box<str>]>>>;
706
707	async fn create_profile(
708		&self,
709		tn_id: TnId,
710		profile: &Profile<&str>,
711		etag: &str,
712	) -> ClResult<()>;
713	async fn update_profile(
714		&self,
715		tn_id: TnId,
716		id_tag: &str,
717		profile: &UpdateProfileData,
718	) -> ClResult<()>;
719
720	/// Reads the public key of a profile
721	///
722	/// Returns a `(public key, expiration)` tuple.
723	async fn read_profile_public_key(
724		&self,
725		id_tag: &str,
726		key_id: &str,
727	) -> ClResult<(Box<str>, Timestamp)>;
728	async fn add_profile_public_key(
729		&self,
730		id_tag: &str,
731		key_id: &str,
732		public_key: &str,
733	) -> ClResult<()>;
734	/// Process profile refresh
735	/// callback(tn_id: TnId, id_tag: &str, etag: Option<&str>)
736	//async fn process_profile_refresh(&self, callback: FnOnce<(TnId, &str, Option<&str>)>);
737	//async fn process_profile_refresh<'a, F>(&self, callback: F)
738	//	where F: FnOnce(TnId, &'a str, Option<&'a str>) -> ClResult<()> + Send;
739	async fn process_profile_refresh<'a>(
740		&self,
741		callback: Box<dyn Fn(TnId, &'a str, Option<&'a str>) -> ClResult<()> + Send>,
742	);
743
744	/// List stale profiles that need refreshing
745	///
746	/// Returns profiles where `synced_at IS NULL OR synced_at < now - max_age_secs`.
747	/// Returns `Vec<(tn_id, id_tag, etag)>` tuples for conditional refresh requests.
748	async fn list_stale_profiles(
749		&self,
750		max_age_secs: i64,
751		limit: u32,
752	) -> ClResult<Vec<(TnId, Box<str>, Option<Box<str>>)>>;
753
754	// Action management
755	//*******************
756	async fn get_action_id(&self, tn_id: TnId, a_id: u64) -> ClResult<Box<str>>;
757	async fn list_actions(
758		&self,
759		tn_id: TnId,
760		opts: &ListActionOptions,
761	) -> ClResult<Vec<ActionView>>;
762	async fn list_action_tokens(
763		&self,
764		tn_id: TnId,
765		opts: &ListActionOptions,
766	) -> ClResult<Box<[Box<str>]>>;
767
768	async fn create_action(
769		&self,
770		tn_id: TnId,
771		action: &Action<&str>,
772		key: Option<&str>,
773	) -> ClResult<ActionId<Box<str>>>;
774
775	async fn finalize_action(
776		&self,
777		tn_id: TnId,
778		a_id: u64,
779		action_id: &str,
780		options: FinalizeActionOptions<'_>,
781	) -> ClResult<()>;
782
783	async fn create_inbound_action(
784		&self,
785		tn_id: TnId,
786		action_id: &str,
787		token: &str,
788		ack_token: Option<&str>,
789	) -> ClResult<()>;
790
791	/// Get the root_id of an action
792	async fn get_action_root_id(&self, tn_id: TnId, action_id: &str) -> ClResult<Box<str>>;
793
794	/// Get action data (subject, reaction count, comment count)
795	async fn get_action_data(&self, tn_id: TnId, action_id: &str) -> ClResult<Option<ActionData>>;
796
797	/// Get action by key
798	async fn get_action_by_key(
799		&self,
800		tn_id: TnId,
801		action_key: &str,
802	) -> ClResult<Option<Action<Box<str>>>>;
803
804	/// Store action token for federation (called when action is created)
805	async fn store_action_token(&self, tn_id: TnId, action_id: &str, token: &str) -> ClResult<()>;
806
807	/// Get action token for federation
808	async fn get_action_token(&self, tn_id: TnId, action_id: &str) -> ClResult<Option<Box<str>>>;
809
810	/// Update action data (subject, reactions, comments, status)
811	async fn update_action_data(
812		&self,
813		tn_id: TnId,
814		action_id: &str,
815		opts: &UpdateActionDataOptions,
816	) -> ClResult<()>;
817
818	/// Update inbound action status
819	async fn update_inbound_action(
820		&self,
821		tn_id: TnId,
822		action_id: &str,
823		status: Option<char>,
824	) -> ClResult<()>;
825
826	/// Get related action tokens by APRV action_id
827	/// Returns list of (action_id, token) pairs for actions that have ack = aprv_action_id
828	async fn get_related_action_tokens(
829		&self,
830		tn_id: TnId,
831		aprv_action_id: &str,
832	) -> ClResult<Vec<(Box<str>, Box<str>)>>;
833
834	/// Create outbound action
835	async fn create_outbound_action(
836		&self,
837		tn_id: TnId,
838		action_id: &str,
839		token: &str,
840		opts: &CreateOutboundActionOptions,
841	) -> ClResult<()>;
842
843	// File management
844	//*****************
845	async fn get_file_id(&self, tn_id: TnId, f_id: u64) -> ClResult<Box<str>>;
846	async fn list_files(&self, tn_id: TnId, opts: &ListFileOptions) -> ClResult<Vec<FileView>>;
847	async fn list_file_variants(
848		&self,
849		tn_id: TnId,
850		file_id: FileId<&str>,
851	) -> ClResult<Vec<FileVariant<Box<str>>>>;
852	/// List locally available variant names for a file (only those marked available)
853	async fn list_available_variants(&self, tn_id: TnId, file_id: &str) -> ClResult<Vec<Box<str>>>;
854	async fn read_file_variant(
855		&self,
856		tn_id: TnId,
857		variant_id: &str,
858	) -> ClResult<FileVariant<Box<str>>>;
859	/// Look up the file_id for a given variant_id
860	async fn read_file_id_by_variant(&self, tn_id: TnId, variant_id: &str) -> ClResult<Box<str>>;
861	/// Look up the internal f_id for a given file_id (for adding variants to existing files)
862	async fn read_f_id_by_file_id(&self, tn_id: TnId, file_id: &str) -> ClResult<u64>;
863	async fn create_file(&self, tn_id: TnId, opts: CreateFile) -> ClResult<FileId<Box<str>>>;
864	async fn create_file_variant<'a>(
865		&'a self,
866		tn_id: TnId,
867		f_id: u64,
868		opts: FileVariant<&'a str>,
869	) -> ClResult<&'a str>;
870	async fn update_file_id(&self, tn_id: TnId, f_id: u64, file_id: &str) -> ClResult<()>;
871
872	/// Finalize a pending file - sets file_id and transitions status from 'P' to 'A' atomically
873	async fn finalize_file(&self, tn_id: TnId, f_id: u64, file_id: &str) -> ClResult<()>;
874
875	// Task scheduler
876	//****************
877	async fn list_tasks(&self, opts: ListTaskOptions) -> ClResult<Vec<Task>>;
878	async fn list_task_ids(&self, kind: &str, keys: &[Box<str>]) -> ClResult<Vec<u64>>;
879	async fn create_task(
880		&self,
881		kind: &'static str,
882		key: Option<&str>,
883		input: &str,
884		deps: &[u64],
885	) -> ClResult<u64>;
886	async fn update_task_finished(&self, task_id: u64, output: &str) -> ClResult<()>;
887	async fn update_task_error(
888		&self,
889		task_id: u64,
890		output: &str,
891		next_at: Option<Timestamp>,
892	) -> ClResult<()>;
893
894	/// Find a pending task by its key
895	async fn find_task_by_key(&self, key: &str) -> ClResult<Option<Task>>;
896
897	/// Update task fields with partial updates
898	async fn update_task(&self, task_id: u64, patch: &TaskPatch) -> ClResult<()>;
899
900	// Phase 1: Profile Management
901	//****************************
902	/// Get a single profile by id_tag
903	async fn get_profile_info(&self, tn_id: TnId, id_tag: &str) -> ClResult<ProfileData>;
904
905	// Phase 2: Action Management
906	//***************************
907	/// Get a single action by action_id
908	async fn get_action(&self, tn_id: TnId, action_id: &str) -> ClResult<Option<ActionView>>;
909
910	/// Update action content and attachments (if not yet federated)
911	async fn update_action(
912		&self,
913		tn_id: TnId,
914		action_id: &str,
915		content: Option<&str>,
916		attachments: Option<&[&str]>,
917	) -> ClResult<()>;
918
919	/// Delete an action (soft delete with cleanup)
920	async fn delete_action(&self, tn_id: TnId, action_id: &str) -> ClResult<()>;
921
922	/// Add a reaction to an action
923	async fn add_reaction(
924		&self,
925		tn_id: TnId,
926		action_id: &str,
927		reactor_id_tag: &str,
928		reaction_type: &str,
929		content: Option<&str>,
930	) -> ClResult<()>;
931
932	/// List all reactions for an action
933	async fn list_reactions(&self, tn_id: TnId, action_id: &str) -> ClResult<Vec<ReactionData>>;
934
935	// Phase 2: File Management Enhancements
936	//**************************************
937	/// Delete a file (set status to 'D')
938	async fn delete_file(&self, tn_id: TnId, file_id: &str) -> ClResult<()>;
939
940	// Settings Management
941	//*********************
942	/// List all settings for a tenant, optionally filtered by prefix
943	async fn list_settings(
944		&self,
945		tn_id: TnId,
946		prefix: Option<&[String]>,
947	) -> ClResult<std::collections::HashMap<String, serde_json::Value>>;
948
949	/// Read a single setting by name
950	async fn read_setting(&self, tn_id: TnId, name: &str) -> ClResult<Option<serde_json::Value>>;
951
952	/// Update or delete a setting (None = delete)
953	async fn update_setting(
954		&self,
955		tn_id: TnId,
956		name: &str,
957		value: Option<serde_json::Value>,
958	) -> ClResult<()>;
959
960	// Reference / Bookmark Management
961	//********************************
962	/// List all references for a tenant
963	async fn list_refs(&self, tn_id: TnId, opts: &ListRefsOptions) -> ClResult<Vec<RefData>>;
964
965	/// Get a specific reference by ID
966	async fn get_ref(&self, tn_id: TnId, ref_id: &str) -> ClResult<Option<(Box<str>, Box<str>)>>;
967
968	/// Create a new reference
969	async fn create_ref(
970		&self,
971		tn_id: TnId,
972		ref_id: &str,
973		opts: &CreateRefOptions,
974	) -> ClResult<RefData>;
975
976	/// Delete a reference
977	async fn delete_ref(&self, tn_id: TnId, ref_id: &str) -> ClResult<()>;
978
979	/// Use/consume a reference - validates type, expiration, counter, decrements counter
980	/// Returns (TnId, id_tag, RefData) of the tenant that owns this ref
981	async fn use_ref(
982		&self,
983		ref_id: &str,
984		expected_types: &[&str],
985	) -> ClResult<(TnId, Box<str>, RefData)>;
986
987	/// Validate a reference without consuming it - checks type, expiration, counter
988	/// Returns (TnId, id_tag, RefData) of the tenant that owns this ref if valid
989	async fn validate_ref(
990		&self,
991		ref_id: &str,
992		expected_types: &[&str],
993	) -> ClResult<(TnId, Box<str>, RefData)>;
994
995	// Tag Management
996	//***************
997	/// List all tags for a tenant
998	///
999	/// # Arguments
1000	/// * `tn_id` - Tenant ID
1001	/// * `prefix` - Optional prefix filter
1002	/// * `with_counts` - If true, include file counts per tag
1003	/// * `limit` - Optional limit on number of tags returned
1004	async fn list_tags(
1005		&self,
1006		tn_id: TnId,
1007		prefix: Option<&str>,
1008		with_counts: bool,
1009		limit: Option<u32>,
1010	) -> ClResult<Vec<TagInfo>>;
1011
1012	/// Add a tag to a file
1013	async fn add_tag(&self, tn_id: TnId, file_id: &str, tag: &str) -> ClResult<Vec<String>>;
1014
1015	/// Remove a tag from a file
1016	async fn remove_tag(&self, tn_id: TnId, file_id: &str, tag: &str) -> ClResult<Vec<String>>;
1017
1018	// File Management Enhancements
1019	//****************************
1020	/// Update file metadata (name, visibility, status)
1021	async fn update_file_data(
1022		&self,
1023		tn_id: TnId,
1024		file_id: &str,
1025		opts: &UpdateFileOptions,
1026	) -> ClResult<()>;
1027
1028	/// Read file metadata
1029	async fn read_file(&self, tn_id: TnId, file_id: &str) -> ClResult<Option<FileView>>;
1030
1031	// File User Data (per-user file activity tracking)
1032	//**************************************************
1033
1034	/// Record file access for a user (upserts record, updates accessed_at timestamp)
1035	async fn record_file_access(&self, tn_id: TnId, id_tag: &str, file_id: &str) -> ClResult<()>;
1036
1037	/// Record file modification for a user (upserts record, updates modified_at timestamp)
1038	async fn record_file_modification(
1039		&self,
1040		tn_id: TnId,
1041		id_tag: &str,
1042		file_id: &str,
1043	) -> ClResult<()>;
1044
1045	/// Update file user data (pinned/starred status)
1046	async fn update_file_user_data(
1047		&self,
1048		tn_id: TnId,
1049		id_tag: &str,
1050		file_id: &str,
1051		pinned: Option<bool>,
1052		starred: Option<bool>,
1053	) -> ClResult<FileUserData>;
1054
1055	/// Get file user data for a specific file
1056	async fn get_file_user_data(
1057		&self,
1058		tn_id: TnId,
1059		id_tag: &str,
1060		file_id: &str,
1061	) -> ClResult<Option<FileUserData>>;
1062
1063	// Push Subscription Management
1064	//*****************************
1065
1066	/// List all push subscriptions for a tenant (user)
1067	///
1068	/// Returns all active push subscriptions for this tenant.
1069	/// Each tenant represents a user, so this returns all their device subscriptions.
1070	async fn list_push_subscriptions(&self, tn_id: TnId) -> ClResult<Vec<PushSubscription>>;
1071
1072	/// Create a new push subscription
1073	///
1074	/// Stores a Web Push subscription for a tenant. The subscription contains
1075	/// the endpoint URL and encryption keys needed to send push notifications.
1076	/// Returns the generated subscription ID.
1077	async fn create_push_subscription(
1078		&self,
1079		tn_id: TnId,
1080		subscription: &PushSubscriptionData,
1081	) -> ClResult<u64>;
1082
1083	/// Delete a push subscription by ID
1084	///
1085	/// Removes a push subscription. Called when a subscription becomes invalid
1086	/// (e.g., 410 Gone response from push service) or when user unsubscribes.
1087	async fn delete_push_subscription(&self, tn_id: TnId, subscription_id: u64) -> ClResult<()>;
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092	use super::*;
1093	use serde_urlencoded;
1094
1095	#[test]
1096	fn test_deserialize_list_action_options_with_multiple_statuses() {
1097		let query = "status=C,N&type=POST,REPLY";
1098		let opts: ListActionOptions =
1099			serde_urlencoded::from_str(query).expect("should deserialize");
1100
1101		assert!(opts.status.is_some());
1102		let statuses = opts.status.expect("status should be Some");
1103		assert_eq!(statuses.len(), 2);
1104		assert_eq!(statuses[0].as_str(), "C");
1105		assert_eq!(statuses[1].as_str(), "N");
1106
1107		assert!(opts.typ.is_some());
1108		let types = opts.typ.expect("type should be Some");
1109		assert_eq!(types.len(), 2);
1110		assert_eq!(types[0].as_str(), "POST");
1111		assert_eq!(types[1].as_str(), "REPLY");
1112	}
1113
1114	#[test]
1115	fn test_deserialize_list_action_options_without_status() {
1116		let query = "issuer=alice";
1117		let opts: ListActionOptions =
1118			serde_urlencoded::from_str(query).expect("should deserialize");
1119
1120		assert!(opts.status.is_none());
1121		assert!(opts.typ.is_none());
1122		assert_eq!(opts.issuer.as_deref(), Some("alice"));
1123	}
1124
1125	#[test]
1126	fn test_deserialize_list_action_options_single_status() {
1127		let query = "status=C";
1128		let opts: ListActionOptions =
1129			serde_urlencoded::from_str(query).expect("should deserialize");
1130
1131		assert!(opts.status.is_some());
1132		let statuses = opts.status.expect("status should be Some");
1133		assert_eq!(statuses.len(), 1);
1134		assert_eq!(statuses[0].as_str(), "C");
1135	}
1136}
1137
1138// vim: ts=4