qm_entity/
owned.rs

1use std::{
2    borrow::Cow,
3    future::{Future, IntoFuture},
4    marker::PhantomData,
5    pin::Pin,
6    str::FromStr,
7    sync::Arc,
8};
9
10use async_graphql::{Description, InputValueError, InputValueResult, Scalar, ScalarType, Value};
11use chrono::{DateTime, Utc};
12use futures::{StreamExt as _, TryStreamExt as _};
13use serde::{de::DeserializeOwned, Deserialize, Serialize};
14
15use qm_mongodb::{
16    bson::{
17        doc, oid::ObjectId, serde_helpers::chrono_datetime_as_bson_datetime, to_bson, Bson,
18        Document, Uuid,
19    },
20    options::FindOptions,
21    Collection, Database,
22};
23
24use crate::{
25    error::EntityError,
26    ids::{
27        CustomerId, CustomerOrOrganization, CustomerResourceId, InstitutionId,
28        InstitutionResourceId, OrganizationId, OrganizationOrInstitution, OrganizationResourceId,
29        OwnerId,
30    },
31    model::ListFilter,
32};
33
34const EMPTY_ID: &str = "000000000000000000000000";
35const DEFAULT_PAGE_LIMIT: i64 = 100;
36
37/// External representation of Object ID type in MongoDB.
38#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, Description)]
39pub struct Id(ObjectId);
40
41impl FromStr for Id {
42    type Err = ();
43
44    fn from_str(s: &str) -> Result<Self, Self::Err> {
45        parse_object_id(s)?.ok_or(())
46    }
47}
48
49fn parse_object_id(id: &str) -> Result<Option<Id>, ()> {
50    if id == EMPTY_ID {
51        Ok(None)
52    } else {
53        Ok(Some(ObjectId::from_str(id).map(Id).map_err(|_| ())?))
54    }
55}
56
57#[Scalar]
58impl ScalarType for Id {
59    fn parse(value: Value) -> InputValueResult<Self> {
60        if let Value::String(value) = &value {
61            Ok(Self::from_str(value).map_err(|_| InputValueError::custom("parse error"))?)
62        } else {
63            Err(InputValueError::expected_type(value))
64        }
65    }
66
67    fn to_value(&self) -> Value {
68        Value::String(self.0.to_hex())
69    }
70}
71
72type ID = Id;
73
74/// Entity Id.
75#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, Description)]
76#[graphql(name = "EntityId")]
77pub struct GraphQLId {
78    #[graphql(flatten)]
79    id: ID,
80}
81
82impl FromStr for GraphQLId {
83    type Err = ();
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        parse_object_id(s)?.map(|id| Self { id }).ok_or(())
87    }
88}
89
90impl AsRef<ObjectId> for GraphQLId {
91    fn as_ref(&self) -> &ObjectId {
92        &self.id.0
93    }
94}
95
96#[Scalar]
97impl ScalarType for GraphQLId {
98    fn parse(value: Value) -> InputValueResult<Self> {
99        if let Value::String(value) = &value {
100            Ok(Self::from_str(value).map_err(|_| InputValueError::custom("parse error"))?)
101        } else {
102            Err(InputValueError::expected_type(value))
103        }
104    }
105
106    fn to_value(&self) -> Value {
107        Value::String(self.id.0.to_hex())
108    }
109}
110
111impl From<Id> for GraphQLId {
112    fn from(id: Id) -> Self {
113        Self { id }
114    }
115}
116
117pub trait ToMongoFilterMany {
118    fn to_mongo_filter_many(&self) -> Option<Document>;
119}
120
121impl ToMongoFilterMany for () {
122    fn to_mongo_filter_many(&self) -> Option<Document> {
123        None
124    }
125}
126
127impl ToMongoFilterMany for Option<Document> {
128    fn to_mongo_filter_many(&self) -> Option<Document> {
129        self.clone()
130    }
131}
132
133impl<T> ToMongoFilterMany for Option<T>
134where
135    T: ToMongoFilterMany,
136{
137    fn to_mongo_filter_many(&self) -> Option<Document> {
138        self.as_ref().and_then(|v| v.to_mongo_filter_many())
139    }
140}
141
142impl ToMongoFilterMany for CustomerId {
143    fn to_mongo_filter_many(&self) -> Option<Document> {
144        let cid = self.unzip();
145        Some(doc! { "owner.cid": cid })
146    }
147}
148
149impl ToMongoFilterMany for OrganizationId {
150    fn to_mongo_filter_many(&self) -> Option<Document> {
151        let (cid, oid) = self.unzip();
152        Some(doc! { "owner.cid": cid, "owner.oid": oid })
153    }
154}
155
156impl ToMongoFilterMany for InstitutionId {
157    fn to_mongo_filter_many(&self) -> Option<Document> {
158        let (cid, oid, iid) = self.unzip();
159        Some(doc! { "owner.cid": cid, "owner.oid": oid, "owner.iid": iid })
160    }
161}
162
163impl ToMongoFilterMany for CustomerOrOrganization {
164    fn to_mongo_filter_many(&self) -> Option<Document> {
165        match self {
166            Self::Customer(v) => v.to_mongo_filter_many(),
167            Self::Organization(v) => v.to_mongo_filter_many(),
168        }
169    }
170}
171
172impl ToMongoFilterMany for OrganizationOrInstitution {
173    fn to_mongo_filter_many(&self) -> Option<Document> {
174        match self {
175            Self::Institution(v) => v.to_mongo_filter_many(),
176            Self::Organization(v) => v.to_mongo_filter_many(),
177        }
178    }
179}
180
181pub trait ToMongoFilterOne {
182    fn to_mongo_filter_one(&self) -> Document;
183}
184
185impl ToMongoFilterOne for Document {
186    fn to_mongo_filter_one(&self) -> Document {
187        self.clone()
188    }
189}
190
191impl ToMongoFilterOne for CustomerResourceId {
192    fn to_mongo_filter_one(&self) -> Document {
193        let (.., id) = self.unzip();
194        doc! { "_id": id }
195    }
196}
197
198impl ToMongoFilterOne for OrganizationResourceId {
199    fn to_mongo_filter_one(&self) -> Document {
200        doc! { "_id": self.id() }
201    }
202}
203
204impl ToMongoFilterOne for InstitutionResourceId {
205    fn to_mongo_filter_one(&self) -> Document {
206        let (.., id) = self.unzip();
207        doc! { "_id": id }
208    }
209}
210
211impl ToMongoFilterOne for CustomerId {
212    fn to_mongo_filter_one(&self) -> Document {
213        doc! { "_id": self.unzip() }
214    }
215}
216
217impl ToMongoFilterOne for OrganizationId {
218    fn to_mongo_filter_one(&self) -> Document {
219        doc! { "_id": self.id() }
220    }
221}
222
223impl ToMongoFilterOne for InstitutionId {
224    fn to_mongo_filter_one(&self) -> Document {
225        doc! { "_id": self.id() }
226    }
227}
228
229impl ToMongoFilterOne for CustomerOrOrganization {
230    fn to_mongo_filter_one(&self) -> Document {
231        match self {
232            Self::Customer(v) => v.to_mongo_filter_one(),
233            Self::Organization(v) => v.to_mongo_filter_one(),
234        }
235    }
236}
237
238impl ToMongoFilterOne for OrganizationOrInstitution {
239    fn to_mongo_filter_one(&self) -> Document {
240        match self {
241            Self::Institution(v) => v.to_mongo_filter_one(),
242            Self::Organization(v) => v.to_mongo_filter_one(),
243        }
244    }
245}
246
247pub trait ToMongoFilterExact {
248    fn to_mongo_filter_exact(&self) -> Result<Document, EntityError>;
249}
250
251pub struct ResourcesFilter<'a, I>(pub &'a [I])
252where
253    I: ToMongoFilterOne;
254impl<I> ToMongoFilterExact for ResourcesFilter<'_, I>
255where
256    I: ToMongoFilterOne,
257{
258    fn to_mongo_filter_exact(&self) -> Result<Document, EntityError> {
259        if self.0.is_empty() {
260            return Err(EntityError::NotEmpty);
261        }
262        if self.0.len() == 1 {
263            return Ok(self.0.first().unwrap().to_mongo_filter_one());
264        }
265        let items: Vec<Document> = self.0.iter().map(|v| v.to_mongo_filter_one()).collect();
266        Ok(doc! {
267            "$or": items,
268        })
269    }
270}
271
272pub trait AsMongoId {
273    fn as_mongo_id(&self) -> ObjectId;
274}
275
276pub trait FromMongoId: Sized {
277    fn from_mongo_id(old_id: Self, bson: Bson) -> Option<Self>;
278}
279
280pub trait IsMongoInsert {
281    fn is_mongo_insert(&self) -> bool;
282}
283
284#[derive(Debug, Deserialize, Serialize)]
285pub struct Entity<T> {
286    id: ID,
287    #[serde(flatten)]
288    fields: T,
289    #[serde(flatten)]
290    defaults: Defaults,
291}
292
293#[derive(Debug, Default, Deserialize, Serialize)]
294pub struct Page<I> {
295    pub items: Vec<I>,
296    pub skip: u64,
297    pub limit: Option<i64>,
298    pub total: usize,
299}
300
301impl<I> Page<I> {
302    /// Empty page.
303    pub fn empty() -> Self {
304        Self {
305            items: vec![],
306            total: 0,
307            skip: 0,
308            limit: Some(DEFAULT_PAGE_LIMIT),
309        }
310    }
311
312    /// Returns page index.
313    pub fn index(&self) -> u64 {
314        if let Some(limit) = self.limit.filter(|l| *l > 0).map(|l| l as u64) {
315            self.skip / limit
316        } else {
317            0
318        }
319    }
320
321    /// Returns page count.
322    pub fn count(&self) -> usize {
323        if let Some(limit) = self.limit.filter(|l| *l > 0).map(|l| l as usize) {
324            self.total.div_ceil(limit)
325        } else {
326            0
327        }
328    }
329}
330
331#[derive(Default)]
332pub struct PageInfo {
333    skip: Option<u64>,
334    limit: Option<i64>,
335}
336
337impl TryFrom<ListFilter> for PageInfo {
338    type Error = EntityError;
339
340    fn try_from(value: ListFilter) -> Result<Self, Self::Error> {
341        let limit = value.limit.map(|l| l as i64).unwrap_or(DEFAULT_PAGE_LIMIT);
342        Ok(Self {
343            skip: value.page.map(|page| limit as u64 * page as u64),
344            limit: Some(limit),
345        })
346    }
347}
348
349impl TryFrom<Option<ListFilter>> for PageInfo {
350    type Error = EntityError;
351
352    fn try_from(value: Option<ListFilter>) -> Result<Self, Self::Error> {
353        value
354            .map(|v| v.try_into())
355            .unwrap_or_else(|| Ok(Default::default()))
356    }
357}
358
359pub trait UpdateEntity<T: Clone> {
360    fn update_entity(self, entity: &T) -> Result<Cow<'_, T>, EntityError>;
361}
362
363#[derive(Debug, Deserialize, Serialize, Clone)]
364pub struct EntityOwned<T, ID = Id> {
365    #[serde(rename = "_id")]
366    pub id: ID,
367    pub owner: Arc<OwnerId>,
368    #[serde(flatten)]
369    pub fields: T,
370    #[serde(flatten)]
371    pub defaults: Arc<Defaults>,
372}
373
374impl<T> EntityOwned<T>
375where
376    T: DeserializeOwned + Serialize + MongoCollection + Send + Sync + Unpin,
377{
378    pub async fn create(
379        db: &Database,
380        owner: impl Into<OwnerId>,
381        fields: T,
382        user_id: Uuid,
383    ) -> Result<Self, EntityError> {
384        #[derive(Serialize)]
385        struct CreateOwnedEntity<'f, F> {
386            owner: Arc<OwnerId>,
387            #[serde(flatten)]
388            fields: &'f F,
389            #[serde(flatten)]
390            defaults: Arc<Defaults>,
391        }
392
393        let owner = Arc::new(owner.into());
394        let defaults = Arc::new(Defaults::now(user_id));
395
396        T::mongo_collection(db)
397            .insert_one(CreateOwnedEntity {
398                owner: owner.clone(),
399                fields: &fields,
400                defaults: defaults.clone(),
401            })
402            .await?
403            .inserted_id
404            .as_object_id()
405            .map(Id)
406            .ok_or(EntityError::NoId)
407            .map(|id| Self {
408                id,
409                owner,
410                fields,
411                defaults,
412            })
413    }
414}
415
416impl<T, ID> EntityOwned<T, ID>
417where
418    T: DeserializeOwned + Serialize + MongoCollection + Send + Sync + Unpin,
419    ID: DeserializeOwned + Serialize + Send + Sync + Unpin,
420{
421    /// Query owned entities
422    pub fn query(db: &Database) -> Query<'_, T, ID> {
423        Query::new(db)
424    }
425
426    pub async fn list(
427        db: &Database,
428        filter: impl ToMongoFilterMany,
429    ) -> Result<Vec<Self>, EntityError> {
430        T::mongo_collection(db)
431            .find(filter.to_mongo_filter_many().unwrap_or_default())
432            .await?
433            .try_collect()
434            .await
435            .map_err(From::from)
436    }
437
438    pub async fn page(
439        db: &Database,
440        filter: impl ToMongoFilterMany,
441        page_selector: impl TryInto<PageInfo, Error = EntityError>,
442    ) -> Result<Page<Self>, EntityError> {
443        Self::page_filter(
444            db,
445            filter.to_mongo_filter_many().unwrap_or_default(),
446            page_selector,
447        )
448        .await
449    }
450
451    pub async fn list_exact(
452        db: &Database,
453        filter: impl ToMongoFilterExact,
454    ) -> Result<Vec<Self>, EntityError> {
455        T::mongo_collection(db)
456            .find(filter.to_mongo_filter_exact()?)
457            .await?
458            .try_collect()
459            .await
460            .map_err(From::from)
461    }
462
463    pub async fn page_exact(
464        db: &Database,
465        filter: impl ToMongoFilterExact,
466        page_selector: impl TryInto<PageInfo, Error = EntityError>,
467    ) -> Result<Page<Self>, EntityError> {
468        Self::page_filter(db, filter.to_mongo_filter_exact()?, page_selector).await
469    }
470
471    pub async fn by_id(
472        db: &Database,
473        id: impl ToMongoFilterOne,
474    ) -> Result<Option<Self>, EntityError> {
475        T::mongo_collection(db)
476            .find_one(id.to_mongo_filter_one())
477            .await
478            .map_err(From::from)
479    }
480
481    pub async fn update(
482        db: &Database,
483        context: impl ToMongoFilterOne,
484        input: impl UpdateEntity<T>,
485        user_id: Uuid,
486    ) -> Result<Self, EntityError>
487    where
488        T: Clone,
489    {
490        let filter = context.to_mongo_filter_one();
491        let Some(mut entity): Option<Self> =
492            T::mongo_collection(db).find_one(filter.clone()).await?
493        else {
494            return Err(EntityError::NotFound);
495        };
496
497        if let Cow::Owned(updated) = input.update_entity(&entity.fields)? {
498            entity.fields = updated;
499            entity.defaults = Arc::new(entity.defaults.update_by(user_id));
500
501            if let Some(filter) = filter.into() {
502                T::mongo_collection::<Self>(db)
503                    .replace_one(filter, &entity)
504                    .await?;
505            }
506        }
507
508        Ok(entity)
509    }
510
511    pub async fn save<C>(
512        db: &Database,
513        context: C,
514        input: impl Into<T>,
515        user_id: Uuid,
516    ) -> Result<bool, EntityError>
517    where
518        T: Clone + std::fmt::Debug,
519        C: ToMongoFilterOne + Into<OwnerId>,
520    {
521        let filter = context.to_mongo_filter_one();
522        #[derive(Debug, Serialize)]
523        struct SaveEntity<F> {
524            owner: OwnerId,
525            #[serde(flatten)]
526            fields: F,
527            #[serde(flatten)]
528            defaults: Arc<Defaults>,
529        }
530        let defaults = Arc::new(Defaults::now(user_id));
531        let entity = SaveEntity {
532            owner: context.into(),
533            fields: input.into(),
534            defaults,
535        };
536        let result = T::mongo_collection::<SaveEntity<_>>(db)
537            .replace_one(filter, &entity)
538            .upsert(true)
539            .await?;
540        Ok(result.modified_count > 0 || result.upserted_id.is_some())
541    }
542
543    pub async fn save_with_id<C, I>(
544        db: &Database,
545        context: C,
546        input: I,
547        user_id: Uuid,
548    ) -> Result<Option<C>, EntityError>
549    where
550        T: Clone + std::fmt::Debug,
551        C: FromMongoId + IsMongoInsert + ToMongoFilterOne + Into<OwnerId> + Clone,
552        I: Into<T> + Send + Sync,
553    {
554        let filter = context.to_mongo_filter_one();
555        Ok(if context.is_mongo_insert() {
556            #[derive(Debug, Serialize)]
557            struct SaveEntity<F> {
558                owner: OwnerId,
559                #[serde(flatten)]
560                fields: F,
561                #[serde(flatten)]
562                defaults: Defaults,
563            }
564            let defaults = Defaults::now(user_id);
565            let entity = SaveEntity {
566                owner: context.clone().into(),
567                fields: input.into(),
568                defaults,
569            };
570            let result = T::mongo_collection::<SaveEntity<T>>(db)
571                .insert_one(&entity)
572                .await?;
573            C::from_mongo_id(context, result.inserted_id)
574        } else {
575            #[derive(Debug, Serialize)]
576            struct SaveEntity<F> {
577                owner: OwnerId,
578                #[serde(flatten)]
579                fields: F,
580                modified: UserModification,
581            }
582            let entity = SaveEntity {
583                owner: context.clone().into(),
584                fields: input.into(),
585                modified: UserModification::now(user_id),
586            };
587            let result = T::mongo_collection::<SaveEntity<T>>(db)
588                .update_one(filter, doc!{ "$set": to_bson(&entity).map_err(|err| EntityError::Bson(err.to_string()))? })
589                .await?;
590            if result.matched_count == 0 {
591                return Err(EntityError::NotFound);
592            }
593            if result.modified_count > 0 {
594                Some(context)
595            } else {
596                None
597            }
598        })
599    }
600
601    pub async fn remove<I>(db: &Database, ids: I) -> Result<i32, EntityError>
602    where
603        I: ToMongoFilterExact,
604    {
605        let result = T::mongo_collection::<Document>(db)
606            .delete_many(ids.to_mongo_filter_exact()?)
607            .await?;
608        Ok(result.deleted_count as i32)
609    }
610
611    pub async fn page_filter(
612        db: &Database,
613        filter: Document,
614        page_selector: impl TryInto<PageInfo, Error = EntityError>,
615    ) -> Result<Page<Self>, EntityError> {
616        let page_info: PageInfo = page_selector.try_into()?;
617        Self::page_filter_sort(db, filter, None, page_info).await
618    }
619
620    pub async fn page_filter_sort(
621        db: &Database,
622        filter: Document,
623        sort: Option<Document>,
624        page_info: PageInfo,
625    ) -> Result<Page<Self>, EntityError> {
626        let total = T::mongo_collection::<Self>(db)
627            .find(filter.clone())
628            .await?
629            .count()
630            .await;
631
632        let limit = page_info.limit;
633
634        if total == 0 {
635            return Ok(if limit.is_some() {
636                Page {
637                    limit,
638                    ..Page::empty()
639                }
640            } else {
641                Page::empty()
642            });
643        }
644
645        T::mongo_collection(db)
646            .find(filter)
647            .with_options(
648                FindOptions::builder()
649                    .limit(limit)
650                    .sort(sort)
651                    .skip(page_info.skip)
652                    .build(),
653            )
654            .await?
655            .try_collect()
656            .await
657            .map(|items| Page {
658                items,
659                total,
660                skip: page_info.skip.unwrap_or_default(),
661                limit,
662            })
663            .map_err(From::from)
664    }
665}
666
667/// Represents a query for a collection of entities.
668pub struct Query<'q, T, ID> {
669    db: &'q Database,
670    filter: Option<Document>,
671    page: Option<PageInfo>,
672    sort: Option<Document>,
673    marker: PhantomData<(T, ID)>,
674}
675
676impl<'q, T, ID> Query<'q, T, ID> {
677    fn new(db: &'q Database) -> Self {
678        Self {
679            db,
680            filter: None,
681            page: None,
682            sort: None,
683            marker: Default::default(),
684        }
685    }
686
687    pub fn filter_exact(mut self, filter: impl ToMongoFilterExact) -> Result<Self, EntityError> {
688        self.filter = Some(filter.to_mongo_filter_exact()?);
689        Ok(self)
690    }
691
692    pub fn filter_many(mut self, filter: impl ToMongoFilterMany) -> Self {
693        self.filter = filter.to_mongo_filter_many();
694        self
695    }
696
697    pub fn filter(mut self, filter: Document) -> Self {
698        self.filter = Some(filter);
699        self
700    }
701
702    pub fn page_selector(
703        mut self,
704        page_selector: impl TryInto<PageInfo, Error = EntityError>,
705    ) -> Result<Self, EntityError> {
706        self.page = Some(page_selector.try_into()?);
707        Ok(self)
708    }
709
710    pub fn page(mut self, page: PageInfo) -> Self {
711        self.page = Some(page);
712        self
713    }
714
715    pub fn sort(mut self, sort: impl Into<Option<Document>>) -> Self {
716        self.sort = sort.into();
717        self
718    }
719}
720
721impl<'q, T, ID> IntoFuture for Query<'q, T, ID>
722where
723    T: DeserializeOwned + Serialize + MongoCollection + Send + Sync + Unpin + 'q,
724    ID: DeserializeOwned + Serialize + Send + Sync + Unpin + 'q,
725{
726    type Output = Result<Page<EntityOwned<T, ID>>, EntityError>;
727
728    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'q>>;
729
730    fn into_future(self) -> Self::IntoFuture {
731        Box::pin(EntityOwned::<T, ID>::page_filter_sort(
732            self.db,
733            self.filter.unwrap_or_default(),
734            self.sort,
735            self.page.unwrap_or_default(),
736        ))
737    }
738}
739
740#[derive(Debug, Deserialize, Serialize)]
741pub struct Defaults {
742    pub created: UserModification,
743    pub modified: UserModification,
744}
745
746impl Defaults {
747    pub fn now(user_id: Uuid) -> Self {
748        let modify = UserModification::now(user_id);
749        Self {
750            created: modify.clone(),
751            modified: modify,
752        }
753    }
754
755    pub fn update_by(&self, user_id: Uuid) -> Self {
756        let modified = UserModification::now(user_id);
757        Self {
758            created: self.created.clone(),
759            modified,
760        }
761    }
762}
763
764#[derive(Debug, Clone, Deserialize, Serialize)]
765pub struct UserModification {
766    #[serde(rename = "uid")]
767    pub user_id: Uuid,
768    #[serde(with = "chrono_datetime_as_bson_datetime")]
769    pub at: DateTime<Utc>,
770}
771
772impl UserModification {
773    pub fn now(user_id: Uuid) -> Self {
774        Self {
775            user_id,
776            at: Utc::now(),
777        }
778    }
779}
780
781pub trait EntityField {
782    type Field<T: Serialize + DeserializeOwned>: Serialize + DeserializeOwned;
783}
784
785#[derive(Default, Clone, PartialEq, Debug)]
786pub struct Optional;
787impl EntityField for Optional {
788    type Field<T: Serialize + DeserializeOwned> = Option<T>;
789}
790
791#[derive(Default, Clone, PartialEq, Debug)]
792pub struct Required;
793impl EntityField for Required {
794    type Field<T: Serialize + DeserializeOwned> = T;
795}
796
797pub trait MongoCollection {
798    const COLLECTION: &'static str;
799
800    fn mongo_collection<T>(db: &Database) -> Collection<T>
801    where
802        T: Send + Sync,
803    {
804        db.collection(Self::COLLECTION)
805    }
806}