//! `GroupDocsService` — the §11.6 shared group-collection DOCS contract. //! //! The cross-app-sharing counterpart to [`crate::DocsService`], analogous to //! [`crate::GroupKvService`]. A script reaches it via the explicit //! `docs::shared_collection("name")` handle. The owner of the data is not //! `cx.app_id`: the impl resolves the **owning group** by walking the calling //! app's ancestor chain for the nearest group that declares the collection //! shared with `kind='docs'`. That ancestry walk is the isolation boundary — //! the trait surface takes **no** group id (owner derivation stays in the impl). //! //! Trust model (§11.6): **reads are open** to any subtree script (anonymous //! public HTTP included); **writes require an authenticated principal** with //! editor+ on the owning group. use async_trait::async_trait; use thiserror::Error; use crate::{DocId, DocRow, DocsListPage, SdkCallCx}; #[async_trait] pub trait GroupDocsService: Send + Sync { async fn create( &self, cx: &SdkCallCx, collection: &str, data: serde_json::Value, ) -> Result; async fn get( &self, cx: &SdkCallCx, collection: &str, id: DocId, ) -> Result, GroupDocsError>; async fn find( &self, cx: &SdkCallCx, collection: &str, filter: serde_json::Value, ) -> Result, GroupDocsError>; async fn find_one( &self, cx: &SdkCallCx, collection: &str, filter: serde_json::Value, ) -> Result, GroupDocsError>; async fn update( &self, cx: &SdkCallCx, collection: &str, id: DocId, data: serde_json::Value, ) -> Result<(), GroupDocsError>; async fn delete( &self, cx: &SdkCallCx, collection: &str, id: DocId, ) -> Result; async fn list( &self, cx: &SdkCallCx, collection: &str, cursor: Option<&str>, limit: u32, ) -> Result; } /// Stub for test bundles that don't exercise shared docs collections. #[derive(Debug, Default, Clone, Copy)] pub struct NoopGroupDocsService; #[async_trait] impl GroupDocsService for NoopGroupDocsService { async fn create( &self, _cx: &SdkCallCx, _collection: &str, _data: serde_json::Value, ) -> Result { Err(GroupDocsError::Backend("group docs is not wired in".into())) } async fn get( &self, _cx: &SdkCallCx, _collection: &str, _id: DocId, ) -> Result, GroupDocsError> { Err(GroupDocsError::Backend("group docs is not wired in".into())) } async fn find( &self, _cx: &SdkCallCx, _collection: &str, _filter: serde_json::Value, ) -> Result, GroupDocsError> { Err(GroupDocsError::Backend("group docs is not wired in".into())) } async fn find_one( &self, _cx: &SdkCallCx, _collection: &str, _filter: serde_json::Value, ) -> Result, GroupDocsError> { Err(GroupDocsError::Backend("group docs is not wired in".into())) } async fn update( &self, _cx: &SdkCallCx, _collection: &str, _id: DocId, _data: serde_json::Value, ) -> Result<(), GroupDocsError> { Err(GroupDocsError::Backend("group docs is not wired in".into())) } async fn delete( &self, _cx: &SdkCallCx, _collection: &str, _id: DocId, ) -> Result { Err(GroupDocsError::Backend("group docs is not wired in".into())) } async fn list( &self, _cx: &SdkCallCx, _collection: &str, _cursor: Option<&str>, _limit: u32, ) -> Result { Err(GroupDocsError::Backend("group docs is not wired in".into())) } } /// Failure modes surfaced to the Rhai bridge. Mirrors [`crate::DocsError`] plus /// the §11.6 `CollectionNotShared` boundary. #[derive(Debug, Error)] pub enum GroupDocsError { #[error("collection name must not be empty")] InvalidCollection, /// No group on the calling app's ancestor chain declares this collection /// shared with `kind='docs'` — the structural "not shared with you" /// boundary (also the outcome for a foreign app). #[error("collection {0:?} is not a shared group docs collection for this app")] CollectionNotShared(String), #[error("document data must be a JSON object")] InvalidData, #[error("invalid filter: {0}")] InvalidFilter(String), #[error("unsupported operator: {0}")] UnsupportedOperator(String), #[error("document not found")] NotFound, /// For reads this is only raised when `cx.principal.is_some()`; for WRITES /// it is also raised when the principal is absent (writes fail closed). #[error("forbidden")] Forbidden, #[error("group docs: data too large ({actual} bytes; limit {limit})")] ValueTooLarge { limit: usize, actual: usize }, /// The owning group's shared-docs store is at its row quota /// (`PICLOUD_GROUP_DOCS_MAX_ROWS`); a new doc is rejected. #[error("group docs: quota exceeded ({actual} rows; limit {limit})")] QuotaExceeded { limit: usize, actual: usize }, /// The owning group's shared-docs store would exceed its total-bytes quota /// (`PICLOUD_GROUP_DOCS_MAX_TOTAL_BYTES`). `actual` is the projected total. #[error("group docs: total-bytes quota exceeded ({actual} bytes; limit {limit})")] TotalBytesQuotaExceeded { limit: u64, actual: u64 }, #[error("group docs backend error: {0}")] Backend(String), }