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}