Skip to main content

alibi_api/plugins/organization/hooks/
organization.rs

1use crate::plugins::organization::types::CreatedOrganizationResponse;
2use alibi_core::AuthResult;
3use alibi_core::CreateMember;
4use alibi_core::CreateOrganization;
5use alibi_core::Member;
6use alibi_core::Organization;
7use alibi_core::UpdateOrganization;
8use alibi_core::utils::json::JsValue;
9use alibi_core::wire::UserView;
10use async_trait::async_trait;
11use indexmap::IndexMap;
12use serde_json::Map;
13use serde_json::Value;
14
15/// The validated draft after creation policies and the initial slug check.
16#[derive(Debug, Clone)]
17pub struct OrganizationDraftContext {
18    pub organization: CreateOrganization,
19    pub user: UserView,
20}
21
22/// The actual creator-member draft and the organization already persisted.
23#[derive(Debug, Clone)]
24pub struct OrganizationMemberDraftContext {
25    pub organization: Organization,
26    pub member: CreateMember,
27    pub user: UserView,
28}
29
30/// Persisted row snapshots used after adding the creator and after creation.
31/// The latter callback runs after default-team callbacks and before selection.
32#[derive(Debug, Clone)]
33pub struct OrganizationCreatedContext {
34    pub organization: Organization,
35    pub member: Member,
36    pub user: UserView,
37}
38
39/// Supported fields merged over the validated draft without revalidation.
40#[derive(Debug, Clone, Default)]
41pub struct OrganizationCreatePatch {
42    pub id: Option<String>,
43    pub name: Option<String>,
44    pub slug: Option<String>,
45    /// `None` retains the draft; `Some(None)` clears its logo.
46    pub logo: Option<Option<String>>,
47    /// `None` retains metadata; `Some(None)` applies the source's falsy-null
48    /// creation policy and omits it. A present empty map stores an empty record.
49    /// This differs from the native store's literal JSON-null write.
50    pub metadata: Option<Option<Map<String, Value>>>,
51}
52impl OrganizationCreatePatch {
53    pub(in crate::plugins) fn apply(self, data: &mut CreateOrganization) {
54        if let Some(id) = self.id {
55            data.id = Some(id);
56        }
57        if let Some(name) = self.name {
58            data.name = name;
59        }
60        if let Some(slug) = self.slug {
61            data.slug = slug;
62        }
63        if let Some(logo) = self.logo {
64            data.logo = logo;
65        }
66        if let Some(metadata) = self.metadata {
67            data.metadata = metadata.map(Value::Object);
68        }
69    }
70}
71
72/// Trusted member-field overrides. The source generates member IDs itself.
73#[derive(Debug, Clone, Default)]
74pub struct OrganizationMemberCreatePatch {
75    pub organization_id: Option<String>,
76    pub user_id: Option<String>,
77    pub role: Option<String>,
78}
79impl OrganizationMemberCreatePatch {
80    pub(in crate::plugins) fn apply(self, data: &mut CreateMember) {
81        if let Some(id) = self.organization_id {
82            data.organization_id = id;
83        }
84        if let Some(id) = self.user_id {
85            data.user_id = id;
86        }
87        if let Some(role) = self.role {
88            data.role = role;
89        }
90    }
91}
92
93/// Creation hooks are awaited at their individual source phases.
94///
95/// Errors retain previous writes; the framework does not wrap this lifecycle in a transaction.
96/// Authority snapshots cannot be changed by an HTTP body or by mutating context. Typed patches
97/// cover bundled model fields, not arbitrary JavaScript columns or direct mutation of callback
98/// arguments. Capture an application store when the callback needs independent persistence or
99/// additional field access.
100#[async_trait]
101pub trait OrganizationCreationHooks: std::fmt::Debug + Send + Sync {
102    async fn before_create(
103        &self,
104        _context: &OrganizationDraftContext,
105    ) -> AuthResult<Option<OrganizationCreatePatch>> {
106        Ok(None)
107    }
108    async fn before_add_member(
109        &self,
110        _context: &OrganizationMemberDraftContext,
111    ) -> AuthResult<Option<OrganizationMemberCreatePatch>> {
112        Ok(None)
113    }
114    async fn after_add_member(&self, _context: &OrganizationCreatedContext) -> AuthResult<()> {
115        Ok(())
116    }
117    async fn after_create(&self, _context: &OrganizationCreatedContext) -> AuthResult<()> {
118        Ok(())
119    }
120}
121
122/// The original authenticated user/session and raw stored organization.
123///
124/// Selection has already been cleared in storage when `before_delete` runs. The same snapshots
125/// reach `after_delete`, after scoped deletion has committed.
126#[derive(Debug, Clone)]
127pub struct OrganizationDeleteContext {
128    pub organization: crate::plugins::organization::types::OrganizationResponse,
129    pub user: UserView,
130    pub session: alibi_core::wire::SessionView,
131    /// Actual supplied values; a trusted header-only call does not invent headers.
132    pub headers: std::collections::HashMap<String, String>,
133    /// None for the trusted header-only helper. HTTP snapshots use the native
134    /// `AuthRequest`'s canonical route path and independent public request parts.
135    pub request: Option<alibi_core::AuthRequest>,
136}
137
138pub(in crate::plugins) struct DeleteInvocation<'a> {
139    pub headers: &'a std::collections::HashMap<String, String>,
140    pub request: Option<&'a alibi_core::AuthRequest>,
141}
142
143/// Awaited deletion phases, without a lifecycle-wide transaction.
144///
145/// A before-hook error retains rows after selection clearing; an after-hook error retains the
146/// committed deletion. Capture an application store for independent operations. Direct JavaScript
147/// argument mutation, global server-API dispatch and continuation after HTTP request cancellation
148/// are separate framework boundaries.
149#[async_trait]
150pub trait OrganizationDeletionHooks: std::fmt::Debug + Send + Sync {
151    async fn before_delete(&self, _context: &OrganizationDeleteContext) -> AuthResult<()> {
152        Ok(())
153    }
154    async fn after_delete(&self, _context: &OrganizationDeleteContext) -> AuthResult<()> {
155        Ok(())
156    }
157}
158
159/// Original validated patch; the source supplies this instead of a stored row.
160#[derive(Debug, Clone)]
161pub struct OrganizationUpdateInput {
162    pub name: Option<String>,
163    pub slug: Option<String>,
164    pub logo: Option<Option<String>>,
165    /// Original JavaScript numbers remain available until adapter serialization.
166    pub metadata: Option<IndexMap<String, JsValue>>,
167}
168
169/// Original validated patch and the authenticated actor and membership snapshots.
170#[derive(Debug, Clone)]
171pub struct OrganizationUpdateContext {
172    pub organization: OrganizationUpdateInput,
173    pub user: UserView,
174    pub member: Member,
175}
176
177/// Parsed adapter output, or `None` when the update found no row.
178/// Authority snapshots retain their original values even if a callback writes.
179#[derive(Debug, Clone)]
180pub struct OrganizationUpdatedContext {
181    pub organization: Option<CreatedOrganizationResponse>,
182    pub user: UserView,
183    pub member: Member,
184}
185
186/// Supported bundled fields merged over validated input without revalidation.
187#[derive(Debug, Clone, Default)]
188pub struct OrganizationUpdatePatch {
189    pub name: Option<String>,
190    pub slug: Option<String>,
191    /// `None` retains the supplied patch; `Some(None)` clears the logo.
192    pub logo: Option<Option<String>>,
193    /// `None` retains input; `Some(None)` stores literal JSON null.
194    /// This intentionally differs from the creation hook's falsy-null policy.
195    pub metadata: Option<Option<IndexMap<String, JsValue>>>,
196}
197impl OrganizationUpdatePatch {
198    ///
199    /// # Errors
200    /// Returns an error when validation, storage, or an application callback fails.
201    pub(in crate::plugins) fn apply(self, data: &mut UpdateOrganization) -> AuthResult<()> {
202        if let Some(name) = self.name {
203            data.name = Some(name);
204        }
205        if let Some(slug) = self.slug {
206            data.slug = Some(slug);
207        }
208        if let Some(logo) = self.logo {
209            data.logo = Some(logo);
210        }
211        if let Some(metadata) = self.metadata {
212            data.metadata = Some(
213                metadata
214                    .map_or(JsValue::Null, JsValue::Object)
215                    .to_json_value()?,
216            );
217        }
218        Ok(())
219    }
220}
221
222/// Trusted application callbacks run after validation/authentication/permission and the initial
223/// slug lookup.
224///
225/// Before errors prevent the update; after errors retain prior writes. This lifecycle is not
226/// wrapped in a transaction. Typed patches cover bundled columns, not arbitrary JavaScript
227/// properties or direct callback-argument mutation. Capture an application store for writes.
228#[async_trait]
229pub trait OrganizationUpdateHooks: std::fmt::Debug + Send + Sync {
230    async fn before_update(
231        &self,
232        _context: &OrganizationUpdateContext,
233    ) -> AuthResult<Option<OrganizationUpdatePatch>> {
234        Ok(None)
235    }
236    async fn after_update(&self, _context: &OrganizationUpdatedContext) -> AuthResult<()> {
237        Ok(())
238    }
239}