qm_customer/schema/
organization.rs

1use std::sync::Arc;
2
3use async_graphql::ComplexObject;
4use async_graphql::ErrorExtensions;
5use async_graphql::ResultExt;
6use async_graphql::{Context, Object};
7
8use qm_entity::error::EntityError;
9use qm_entity::error::EntityResult;
10use qm_entity::exerr;
11use qm_entity::ids::CustomerId;
12use qm_entity::ids::InfraContext;
13use qm_entity::ids::InfraId;
14use qm_entity::ids::OrganizationId;
15use qm_entity::ids::OrganizationIds;
16
17use qm_entity::err;
18use qm_entity::model::ListFilter;
19use qm_mongodb::bson::doc;
20use qm_role::AccessLevel;
21use sqlx::types::Uuid;
22
23use crate::cache::CacheDB;
24
25use crate::cleanup::CleanupTask;
26use crate::cleanup::CleanupTaskType;
27use crate::context::RelatedAuth;
28use crate::context::RelatedPermission;
29use crate::context::RelatedResource;
30use crate::context::RelatedStorage;
31use crate::groups::RelatedBuiltInGroup;
32use crate::marker::Marker;
33use crate::model::CreateOrganizationInput;
34use crate::model::OrganizationData;
35use crate::model::QmCustomer;
36use crate::model::QmOrganization;
37use crate::model::QmOrganizationList;
38use crate::model::UpdateOrganizationInput;
39use crate::mutation::remove_organizations;
40use crate::mutation::update_organization;
41use crate::roles;
42use crate::schema::auth::AuthCtx;
43
44#[ComplexObject]
45impl QmOrganization {
46    async fn id(&self) -> async_graphql::FieldResult<OrganizationId> {
47        Ok(self.into())
48    }
49
50    async fn customer(&self, ctx: &Context<'_>) -> Option<Arc<QmCustomer>> {
51        let cache = ctx.data::<CacheDB>().ok();
52        if cache.is_none() {
53            tracing::warn!("qm::customer::cache::CacheDB is not installed in schema context");
54            return None;
55        }
56        let cache = cache.unwrap();
57        cache.customer_by_id(&self.customer_id).await
58    }
59}
60
61pub struct Ctx<'a, Auth, Store, Resource, Permission>(
62    pub &'a AuthCtx<'a, Auth, Store, Resource, Permission>,
63)
64where
65    Auth: RelatedAuth<Resource, Permission>,
66    Store: RelatedStorage,
67    Resource: RelatedResource,
68    Permission: RelatedPermission;
69impl<'a, Auth, Store, Resource, Permission> Ctx<'a, Auth, Store, Resource, Permission>
70where
71    Auth: RelatedAuth<Resource, Permission>,
72    Store: RelatedStorage,
73    Resource: RelatedResource,
74    Permission: RelatedPermission,
75{
76    pub async fn list(
77        &self,
78        mut context: Option<CustomerId>,
79        filter: Option<ListFilter>,
80        ty: Option<String>,
81    ) -> async_graphql::FieldResult<QmOrganizationList> {
82        context = self.0.enforce_customer_context(context).await.extend()?;
83        Ok(self
84            .0
85            .store
86            .cache_db()
87            .organization_list(context, filter, ty)
88            .await)
89    }
90
91    pub async fn by_id(&self, id: OrganizationId) -> Option<Arc<QmOrganization>> {
92        self.0.store.cache_db().organization_by_id(&id.into()).await
93    }
94
95    pub async fn exists(&self, cid: InfraId, name: Arc<str>) -> bool {
96        self.0
97            .store
98            .cache_db()
99            .organization_by_name(cid, name)
100            .await
101            .is_some()
102    }
103
104    pub async fn create(
105        &self,
106        organization: OrganizationData,
107    ) -> EntityResult<Arc<QmOrganization>> {
108        let user_id = self.0.auth.user_id().unwrap();
109        let cid = organization.0;
110        let name: Arc<str> = Arc::from(organization.1.clone());
111        let ty = organization.2;
112        let lock_key = format!("v1_organization_lock_{:X}_{name}", cid.as_ref());
113        let lock = self.0.store.redis().lock(&lock_key, 5000, 20, 250).await?;
114        let (result, exists) = async {
115            EntityResult::Ok(
116                if let Some(item) = self
117                    .0
118                    .store
119                    .cache_db()
120                    .organization_by_name(cid, name.clone())
121                    .await
122                {
123                    (item, true)
124                } else {
125                    let result = crate::mutation::create_organization(
126                        self.0.store.customer_db().pool(),
127                        organization.3,
128                        &name,
129                        ty.as_deref(),
130                        cid,
131                        user_id,
132                    )
133                    .await?;
134                    let id: OrganizationId = (&result).into();
135                    let access = qm_role::Access::new(AccessLevel::Organization)
136                        .with_fmt_id(Some(&id))
137                        .to_string();
138                    let roles =
139                        roles::ensure(self.0.store.keycloak(), Some(access).into_iter()).await?;
140                    self.0.store.cache_db().user().new_roles(roles).await;
141                    if let Some(producer) = self.0.store.mutation_event_producer() {
142                        producer
143                            .create_event(
144                                &qm_kafka::producer::EventNs::Organization,
145                                "organization",
146                                "sys",
147                                &result,
148                            )
149                            .await?;
150                    }
151                    let organization = Arc::new(result);
152                    self.0
153                        .store
154                        .cache_db()
155                        .infra()
156                        .new_organization(organization.clone())
157                        .await;
158                    (organization, false)
159                },
160            )
161        }
162        .await?;
163        self.0.store.redis().unlock(&lock_key, &lock.id).await?;
164        if exists {
165            return err!(name_conflict::<QmOrganization>(name.to_string()));
166        }
167        Ok(result)
168    }
169
170    pub async fn update(
171        &self,
172        id: OrganizationId,
173        name: String,
174    ) -> EntityResult<Arc<QmOrganization>> {
175        let user_id = self.0.auth.user_id().unwrap();
176        let id: InfraId = id.into();
177        let old = self
178            .0
179            .store
180            .cache_db()
181            .organization_by_id(&id)
182            .await
183            .ok_or(EntityError::not_found_by_field::<QmOrganization>(
184                "name", &name,
185            ))?;
186        let result =
187            update_organization(self.0.store.customer_db().pool(), id, &name, user_id).await?;
188        let new = Arc::new(result);
189        self.0
190            .store
191            .cache_db()
192            .infra()
193            .update_organization(new.clone(), old.as_ref().into())
194            .await;
195        Ok(new)
196    }
197
198    pub async fn remove(&self, ids: OrganizationIds) -> EntityResult<u64> {
199        let v: Vec<i64> = ids.iter().map(OrganizationId::id).collect();
200        let delete_count = remove_organizations(self.0.store.customer_db().pool(), &v).await?;
201        if delete_count != 0 {
202            let id = Uuid::new_v4();
203            self.0
204                .store
205                .cleanup_task_producer()
206                .add_item(&CleanupTask {
207                    id,
208                    ty: CleanupTaskType::Organizations(ids),
209                })
210                .await?;
211            tracing::debug!("emit cleanup task {}", id.to_string());
212            return Ok(delete_count);
213        }
214        Ok(0)
215    }
216}
217
218pub struct OrganizationQueryRoot<Auth, Store, Resource, Permission, BuiltInGroup> {
219    _marker: Marker<Auth, Store, Resource, Permission, BuiltInGroup>,
220}
221
222impl<Auth, Store, Resource, Permission, BuiltInGroup> Default
223    for OrganizationQueryRoot<Auth, Store, Resource, Permission, BuiltInGroup>
224{
225    fn default() -> Self {
226        Self {
227            _marker: std::marker::PhantomData,
228        }
229    }
230}
231
232#[Object]
233impl<Auth, Store, Resource, Permission, BuiltInGroup>
234    OrganizationQueryRoot<Auth, Store, Resource, Permission, BuiltInGroup>
235where
236    Auth: RelatedAuth<Resource, Permission>,
237    Store: RelatedStorage,
238    Resource: RelatedResource,
239    Permission: RelatedPermission,
240    BuiltInGroup: RelatedBuiltInGroup,
241{
242    async fn qm_organization_by_id(
243        &self,
244        ctx: &Context<'_>,
245        id: OrganizationId,
246    ) -> async_graphql::FieldResult<Option<Arc<QmOrganization>>> {
247        Ok(Ctx(
248            &AuthCtx::<'_, Auth, Store, Resource, Permission>::new_with_role(
249                ctx,
250                &qm_role::role!(Resource::organization(), Permission::view()),
251            )
252            .await
253            .extend()?,
254        )
255        .by_id(id)
256        .await)
257    }
258
259    async fn qm_organization_exists(
260        &self,
261        ctx: &Context<'_>,
262        id: CustomerId,
263        name: Arc<str>,
264    ) -> async_graphql::FieldResult<bool> {
265        Ok(Ctx(
266            &AuthCtx::<'_, Auth, Store, Resource, Permission>::new_with_role(
267                ctx,
268                &qm_role::role!(Resource::organization(), Permission::view()),
269            )
270            .await
271            .extend()?,
272        )
273        .exists(id.into(), name)
274        .await)
275    }
276
277    async fn qm_organizations(
278        &self,
279        ctx: &Context<'_>,
280        context: Option<CustomerId>,
281        filter: Option<ListFilter>,
282        ty: Option<String>,
283    ) -> async_graphql::FieldResult<QmOrganizationList> {
284        Ctx(
285            &AuthCtx::<'_, Auth, Store, Resource, Permission>::new_with_role(
286                ctx,
287                &qm_role::role!(Resource::organization(), Permission::list()),
288            )
289            .await?,
290        )
291        .list(context, filter, ty)
292        .await
293        .extend()
294    }
295}
296
297pub struct OrganizationMutationRoot<Auth, Store, Resource, Permission, BuiltInGroup> {
298    _marker: Marker<Auth, Store, Resource, Permission, BuiltInGroup>,
299}
300
301impl<Auth, Store, Resource, Permission, BuiltInGroup> Default
302    for OrganizationMutationRoot<Auth, Store, Resource, Permission, BuiltInGroup>
303{
304    fn default() -> Self {
305        Self {
306            _marker: std::marker::PhantomData,
307        }
308    }
309}
310
311#[Object]
312impl<Auth, Store, Resource, Permission, BuiltInGroup>
313    OrganizationMutationRoot<Auth, Store, Resource, Permission, BuiltInGroup>
314where
315    Auth: RelatedAuth<Resource, Permission>,
316    Store: RelatedStorage,
317    Resource: RelatedResource,
318    Permission: RelatedPermission,
319    BuiltInGroup: RelatedBuiltInGroup,
320{
321    async fn qm_create_organization(
322        &self,
323        ctx: &Context<'_>,
324        context: CustomerId,
325        input: CreateOrganizationInput,
326    ) -> async_graphql::FieldResult<Arc<QmOrganization>> {
327        let auth_ctx = AuthCtx::<Auth, Store, Resource, Permission>::mutate_with_role(
328            ctx,
329            qm_entity::ids::InfraContext::Customer(context),
330            &qm_role::role!(Resource::organization(), Permission::create()),
331        )
332        .await?;
333        Ctx(&auth_ctx)
334            .create(OrganizationData(
335                context.into(),
336                input.name,
337                input.ty,
338                input.id,
339            ))
340            .await
341            .extend()
342    }
343
344    async fn qm_update_organization(
345        &self,
346        ctx: &Context<'_>,
347        context: OrganizationId,
348        input: UpdateOrganizationInput,
349    ) -> async_graphql::FieldResult<Arc<QmOrganization>> {
350        let auth_ctx = AuthCtx::<'_, Auth, Store, Resource, Permission>::new_with_role(
351            ctx,
352            &qm_role::role!(Resource::organization(), Permission::update()),
353        )
354        .await?;
355        auth_ctx
356            .can_mutate(Some(&InfraContext::Organization(context)))
357            .await?;
358        Ctx(&auth_ctx).update(context, input.name).await.extend()
359    }
360
361    async fn qm_remove_organizations(
362        &self,
363        ctx: &Context<'_>,
364        ids: OrganizationIds,
365    ) -> async_graphql::FieldResult<u64> {
366        let auth_ctx = AuthCtx::<'_, Auth, Store, Resource, Permission>::new_with_role(
367            ctx,
368            &qm_role::role!(Resource::organization(), Permission::delete()),
369        )
370        .await?;
371        let cache = auth_ctx.store.cache_db();
372        for id in ids.iter() {
373            let infra_id = id.into();
374            if cache.organization_by_id(&infra_id).await.is_some() {
375                let object_owner = InfraContext::Customer(id.parent());
376                auth_ctx.can_mutate(Some(&object_owner)).await.extend()?;
377            } else {
378                return exerr!(not_found_by_id::<QmOrganization>(id.to_string()));
379            }
380        }
381        Ctx(&auth_ctx).remove(ids).await.extend()
382    }
383}