Skip to main content

qm_entity/
lib.rs

1#![deny(missing_docs)]
2
3//! Entity abstraction layer for quick-microservice.
4//!
5//! This crate provides common entity abstractions, utilities, and traits
6//! for building microservices with MongoDB and PostgreSQL.
7//!
8//! ## Features
9//!
10//! - **Permission Traits**: Define create/update/delete and list/view permissions
11//! - **Collection Helpers**: MongoDB collection wrappers with common operations
12//! - **ID Types**: Standardized ID types and conversions
13//! - **Error Handling**: Entity-specific error types and helpers
14//! - **GraphQL Integration**: Context extraction and error helpers
15//! - **Macros**: Error creation macros for entity operations
16//!
17//! ## Usage
18//!
19//! Define permissions on your entity types:
20//!
21//! \```ignore
22//! use qm_entity::{MutatePermissions, QueryPermissions};
23//!
24//! #[derive(Clone)]
25//! struct MyPermissions;
26//!
27//! impl MutatePermissions for MyPermissions {
28//!     fn create() -> Self { Self }
29//!     fn update() -> Self { Self }
30//!     fn delete() -> Self { Self }
31//! }
32//!
33//! impl QueryPermissions for MyPermissions {
34//!     fn list() -> Self { Self }
35//!     fn view() -> Self { Self }
36//! }
37//! \```
38//!
39//! Use Collection for MongoDB operations:
40//!
41//! \```ignore
42//! use qm_entity::Collection;
43//! use mongodb::bson::oid::ObjectId;
44//!
45//! let collection = Collection(my_mongodb_collection);
46//! let item = collection.by_id(&ObjectId::new()).await?;
47//! \```
48
49use async_graphql::{Context, ErrorExtensions, FieldResult};
50use error::EntityResult;
51use futures::stream::TryStreamExt;
52use serde::{de::DeserializeOwned, Serialize};
53
54use qm_mongodb::{
55    bson::{doc, oid::ObjectId, Document, Uuid},
56    options::FindOptions,
57    results::DeleteResult,
58};
59
60use crate::{
61    ids::ID,
62    model::{ListFilter, ListResult},
63};
64
65/// Error types and helpers.
66pub mod error;
67/// ID type definitions and conversions.
68pub mod ids;
69/// List filter and pagination utilities.
70pub mod list;
71/// Model types for entities.
72pub mod model;
73/// Owned resource types.
74pub mod owned;
75
76/// Trait for defining mutation permissions.
77///
78/// Implement this trait to define which roles can create, update, or delete entities.
79pub trait MutatePermissions {
80    /// Permission for creating entities.
81    fn create() -> Self;
82    /// Permission for updating entities.
83    fn update() -> Self;
84    /// Permission for deleting entities.
85    fn delete() -> Self;
86}
87
88/// Trait for defining query permissions.
89///
90/// Implement this trait to define which roles can list or view entities.
91pub trait QueryPermissions {
92    /// Permission for listing entities.
93    fn list() -> Self;
94    /// Permission for viewing entities.
95    fn view() -> Self;
96}
97
98/// Create a conflict error (HTTP 409).
99///
100/// Use this for errors indicating resource conflicts (e.g., duplicate names).
101pub fn conflict<E>(err: E) -> async_graphql::Error
102where
103    E: ErrorExtensions,
104{
105    err.extend_with(|_err, e| e.set("code", 409))
106}
107
108/// Creates a conflict error for duplicate names.
109pub fn conflicting_name<T>(ty: &str, name: &str) -> Result<T, async_graphql::Error> {
110    Err(conflict(async_graphql::Error::new(format!(
111        "{ty} with the name '{name}' already exists."
112    ))))
113}
114
115/// Create an unauthorized error (HTTP 401).
116///
117/// Use this for authentication/authorization errors.
118pub fn unauthorized<E>(err: E) -> async_graphql::Error
119where
120    E: ErrorExtensions,
121{
122    err.extend_with(|_err, e| e.set("code", 401))
123}
124
125/// Creates an unauthorized error for a named entity.
126pub fn unauthorized_name<T>(ty: &str, name: &str) -> Result<T, async_graphql::Error> {
127    Err(unauthorized(async_graphql::Error::new(format!(
128        "{ty} '{name}' nicht authorisiert."
129    ))))
130}
131
132#[allow(async_fn_in_trait)]
133/// Trait for extracting types from GraphQL context.
134///
135/// Implement this on your user/session types to extract them from the GraphQL context.
136pub trait FromGraphQLContext: Sized {
137    /// Extracts the type from the GraphQL context.
138    async fn from_graphql_context(ctx: &Context<'_>) -> FieldResult<Self>;
139}
140
141/// Trait for admin role detection.
142pub trait IsAdmin {
143    /// Returns whether the user is an admin.
144    fn is_admin(&self) -> bool {
145        false
146    }
147}
148
149/// Trait for support role detection.
150pub trait IsSupport {
151    /// Returns whether the user is support.
152    fn is_support(&self) -> bool {
153        false
154    }
155}
156
157/// Trait for access control.
158pub trait HasAccess {
159    /// Checks if the user has the given access.
160    fn has_access(&self, a: &qm_role::Access) -> bool;
161}
162
163/// Trait for role-based access control.
164///
165/// Implement this trait to check if a user has a specific role with a permission scope.
166pub trait HasRole<R, P>
167where
168    R: std::fmt::Debug + std::marker::Copy + Clone,
169    P: std::fmt::Debug + std::marker::Copy + Clone,
170{
171    /// Checks if the user has the given role with the given permission.
172    fn has_role(&self, r: &R, p: &P) -> bool;
173    /// Checks if the user has the given role object.
174    fn has_role_object(&self, role: &qm_role::Role<R, P>) -> bool;
175}
176
177/// Trait for extracting user ID from session.
178pub trait UserId {
179    /// Returns the user ID if available.
180    fn user_id(&self) -> Option<&sqlx::types::Uuid>;
181}
182
183/// Trait for extracting session access permissions.
184pub trait SessionAccess {
185    /// Returns the session access permissions if available.
186    fn session_access(&self) -> Option<&qm_role::Access>;
187}
188
189/// Trait for converting types to numeric codes.
190pub trait AsNumber {
191    /// Returns the numeric code.
192    fn as_number(&self) -> u32;
193}
194
195/// MongoDB collection wrapper with common CRUD operations.
196///
197/// Provides typed access to MongoDB collections with methods for
198/// finding, listing, saving, and removing documents.
199pub struct Collection<T>(pub qm_mongodb::Collection<T>)
200where
201    T: Send + Sync;
202
203impl<T> AsRef<qm_mongodb::Collection<T>> for Collection<T>
204where
205    T: Send + Sync,
206{
207    fn as_ref(&self) -> &qm_mongodb::Collection<T> {
208        &self.0
209    }
210}
211
212impl<T> Collection<T>
213where
214    T: DeserializeOwned + Send + Sync + Unpin,
215{
216    /// Finds a document by its ID.
217    pub async fn by_id(&self, id: &ObjectId) -> qm_mongodb::error::Result<Option<T>> {
218        self.as_ref().find_one(doc! { "_id": id }).await
219    }
220
221    /// Finds a document by its name field.
222    pub async fn by_name(&self, name: &str) -> qm_mongodb::error::Result<Option<T>> {
223        self.as_ref().find_one(doc! { "name": name }).await
224    }
225
226    /// Finds a document by an arbitrary field and value.
227    pub async fn by_field(&self, field: &str, value: &str) -> qm_mongodb::error::Result<Option<T>> {
228        self.as_ref().find_one(doc! { field: value }).await
229    }
230
231    /// Removes all documents matching string values in a field.
232    pub async fn remove_all_by_strings(
233        &self,
234        field: &str,
235        values: &[String],
236    ) -> qm_mongodb::error::Result<DeleteResult> {
237        self.as_ref()
238            .delete_many(doc! { field: { "$in": values } })
239            .await
240    }
241
242    /// Removes all documents matching UUID values in a field.
243    pub async fn remove_all_by_uuids(
244        &self,
245        field: &str,
246        values: &[&Uuid],
247    ) -> qm_mongodb::error::Result<DeleteResult> {
248        self.as_ref()
249            .delete_many(doc! { field: { "$in": values } })
250            .await
251    }
252
253    /// Finds a document with a customer ID filter.
254    pub async fn by_field_with_customer_filter(
255        &self,
256        cid: &ObjectId,
257        field: &str,
258        value: &str,
259    ) -> qm_mongodb::error::Result<Option<T>> {
260        self.as_ref()
261            .find_one(doc! {
262                "owner.entityId.cid": &cid,
263                field: value
264            })
265            .await
266    }
267
268    /// Lists documents with optional query and filter.
269    pub async fn list(
270        &self,
271        query: Option<Document>,
272        filter: Option<ListFilter>,
273    ) -> qm_mongodb::error::Result<ListResult<T>> {
274        let query = query.unwrap_or_default();
275        let limit = filter
276            .as_ref()
277            .and_then(|filter| filter.limit.as_ref().copied())
278            .unwrap_or(1000) as i64;
279        let page = filter
280            .as_ref()
281            .and_then(|filter| filter.page.as_ref().copied())
282            .unwrap_or(0);
283        let offset = page as u64 * limit as u64;
284        let total = self.as_ref().count_documents(query.clone()).await?;
285        let options = FindOptions::builder().limit(limit).skip(offset).build();
286
287        let items = self
288            .as_ref()
289            .find(query)
290            .with_options(options)
291            .await?
292            .try_collect::<Vec<T>>()
293            .await?;
294        Ok(ListResult {
295            items,
296            limit: Some(limit),
297            total: Some(total as i64),
298            page: Some(page as i64),
299        })
300    }
301}
302
303impl<T> Collection<T>
304where
305    T: Serialize + Send + Sync + Unpin + AsMut<Option<ID>>,
306{
307    /// Saves a document and returns it with the generated ID.
308    pub async fn save(&self, mut value: T) -> qm_mongodb::error::Result<T> {
309        let id: qm_mongodb::bson::Bson = self.as_ref().insert_one(&value).await?.inserted_id;
310        if let qm_mongodb::bson::Bson::ObjectId(cid) = id {
311            *value.as_mut() = Some(cid.into());
312        }
313        Ok(value)
314    }
315}
316
317/// Trait for entity creation logic.
318///
319/// Implement this trait on your entity types to define creation logic
320/// that validates and creates entities based on user context.
321pub trait Create<T, C: UserId> {
322    /// Creates an entity with the given context.
323    fn create(self, ctx: &C) -> EntityResult<T>;
324}
325
326#[doc(hidden)]
327pub mod __private {
328    pub use crate::error::EntityError;
329    #[doc(hidden)]
330    pub use core::result::Result::Err;
331}
332
333/// Macro for creating entity errors.
334#[macro_export]
335macro_rules! err {
336    ($($arg:tt)*) => {
337        $crate::__private::Err($crate::__private::EntityError::$($arg)*)
338    };
339}
340
341/// Macro for creating extended entity errors.
342#[macro_export]
343macro_rules! exerr {
344    ($($arg:tt)*) => {
345        $crate::__private::Err($crate::__private::EntityError::$($arg)*.extend())
346    };
347}