Skip to main content

alibi_core/store/contracts/
organization.rs

1use super::*;
2/// Query parameters for listing organization members.
3#[derive(Debug, Clone, Default)]
4pub struct ListOrganizationMembersParams {
5    /// Organization id whose members should be listed.
6    pub organization_id: String,
7    /// Maximum number of members to return.
8    pub limit: Option<usize>,
9    /// Number of matching members to skip before returning rows.
10    pub offset: Option<usize>,
11    /// Client-visible field name used for sorting.
12    pub sort_by: Option<String>,
13    /// Sort direction (`asc` or `desc`).
14    pub sort_direction: Option<String>,
15    /// Client-visible field name used for filtering.
16    pub filter_field: Option<String>,
17    /// Filter value paired with `filter_field`.
18    pub filter_value: Option<String>,
19    /// Filter operator (`eq`, `ne`, `contains`, `gt`, `gte`, `lt`, `lte`).
20    pub filter_operator: Option<String>,
21}
22
23/// Adapter page with JavaScript numeric limits retained through SQL binding.
24/// Unlike the legacy usize query, an absent sort does not impose an order.
25#[derive(Debug, Clone, Default)]
26pub struct MemberPageQuery {
27    pub organization_id: String,
28    pub limit: Option<f64>,
29    pub offset: Option<f64>,
30    pub sort_by: Option<String>,
31    pub sort_direction: Option<String>,
32    pub filter_field: Option<String>,
33    pub filter_value: Option<String>,
34    pub filter_operator: Option<String>,
35}
36
37#[async_trait]
38pub trait OrganizationStore: Send + Sync {
39    async fn create_organization(&self, org: CreateOrganization) -> AuthResult<Organization>;
40    async fn get_organization_by_id(&self, id: &str) -> AuthResult<Option<Organization>>;
41    async fn get_organization_by_slug(&self, slug: &str) -> AuthResult<Option<Organization>>;
42    /// Fetch multiple organizations by id.
43    ///
44    /// Implementations may return rows in any order. Callers must remap by id
45    /// when response order matters.
46    async fn list_organizations_by_ids(&self, ids: &[String]) -> AuthResult<Vec<Organization>>;
47    async fn update_organization(
48        &self,
49        id: &str,
50        update: UpdateOrganization,
51    ) -> AuthResult<Organization>;
52    /// Apply exactly the supplied organization columns as a database patch.
53    /// Returns absence separately from database errors. Empty patches are sent
54    /// to the adapter rather than converted into timestamp-only updates.
55    /// Custom stores serving the default HTTP update route must implement this
56    /// bounded operation; the default fails closed with NotImplemented.
57    /// Model callbacks belong to `update_organization_if_present` instead.
58    async fn patch_organization_if_present(
59        &self,
60        _id: &str,
61        _update: UpdateOrganization,
62    ) -> AuthResult<Option<Organization>> {
63        Err(AuthError::NotImplemented(
64            "Organization patches are not supported by this store".into(),
65        ))
66    }
67    /// Update a matching organization, retaining adapter model hooks.
68    /// `None` means no row was updated; other storage failures remain errors.
69    /// Custom stores must implement this optional-row operation explicitly.
70    async fn update_organization_if_present(
71        &self,
72        _id: &str,
73        _update: UpdateOrganization,
74    ) -> AuthResult<Option<Organization>> {
75        Err(AuthError::NotImplemented(
76            "Optional organization updates are not supported by this store".into(),
77        ))
78    }
79    /// Delete the organization and its members/invitations atomically.
80    /// Extension rows (teams, roles, API keys) and sessions are retained, matching
81    /// the pinned default adapter. Custom adapters own their constraint policy.
82    async fn delete_organization(&self, id: &str) -> AuthResult<()>;
83    async fn list_user_organizations(&self, user_id: &str) -> AuthResult<Vec<Organization>>;
84}
85
86#[async_trait]
87pub trait MemberStore: Send + Sync {
88    async fn create_member(&self, member: CreateMember) -> AuthResult<Member>;
89    async fn get_member(&self, organization_id: &str, user_id: &str) -> AuthResult<Option<Member>>;
90    async fn get_member_by_id(&self, id: &str) -> AuthResult<Option<Member>>;
91    async fn update_member_role(&self, member_id: &str, role: &str) -> AuthResult<Member>;
92    /// Update a matching member while preserving adapter model hooks.
93    /// Absence is `None`; other storage failures remain errors.
94    async fn update_member_role_if_present(
95        &self,
96        _member_id: &str,
97        _role: &str,
98    ) -> AuthResult<Option<Member>> {
99        Err(AuthError::NotImplemented(
100            "Optional member role updates are not supported by this store".into(),
101        ))
102    }
103    async fn delete_member(&self, member_id: &str) -> AuthResult<()>;
104    /// Delete the authorized original member, then optionally release its team
105    /// memberships atomically. The original scope/user remains authoritative if
106    /// a lifecycle callback independently changes or removes the stored member.
107    /// A missing/ignored member deletion is successful; SQL errors are failures.
108    async fn delete_member_with_context(
109        &self,
110        _member_id: &str,
111        _organization_id: &str,
112        _user_id: &str,
113        _remove_team_members: bool,
114    ) -> AuthResult<()> {
115        Err(AuthError::NotImplemented(
116            "Contextual member deletion is not supported by this store".into(),
117        ))
118    }
119    /// Return the adapter's organization-scoped page without imposing a sort.
120    /// Used for the source's paged last-owner guard independently of total count.
121    async fn list_organization_members_page(
122        &self,
123        _organization_id: &str,
124        _limit: usize,
125    ) -> AuthResult<Vec<Member>> {
126        Err(AuthError::NotImplemented(
127            "Unsorted member pages are not supported by this store".into(),
128        ))
129    }
130    async fn list_organization_members(&self, org_id: &str) -> AuthResult<Vec<Member>>;
131    /// Query organization members with filter, sort, and pagination applied in
132    /// the store when possible.
133    async fn query_organization_members(
134        &self,
135        params: &ListOrganizationMembersParams,
136    ) -> AuthResult<(Vec<Member>, usize)>;
137    /// Apply raw numeric pagination without converting it into the legacy usize API.
138    async fn query_organization_members_page(
139        &self,
140        _params: &MemberPageQuery,
141    ) -> AuthResult<(Vec<Member>, usize)> {
142        Err(AuthError::NotImplemented(
143            "Raw numeric member pages are not supported by this store".into(),
144        ))
145    }
146    async fn count_organization_members(&self, org_id: &str) -> AuthResult<i64>;
147    async fn count_organization_owners(&self, org_id: &str) -> AuthResult<i64>;
148}
149
150/// Trusted persisted-field overrides returned by an invitation creation hook.
151/// They are separate from the stable default CreateInvitation constructor.
152#[derive(Debug, Clone, Default, PartialEq, Eq)]
153pub struct InvitationCreateOptions {
154    pub id: Option<String>,
155    pub status: Option<InvitationStatus>,
156    pub created_at: Option<chrono::DateTime<chrono::Utc>>,
157}
158
159#[async_trait]
160pub trait InvitationStore: Send + Sync {
161    async fn create_invitation_with_options(
162        &self,
163        invitation: CreateInvitation,
164        options: InvitationCreateOptions,
165    ) -> AuthResult<Invitation> {
166        if options == InvitationCreateOptions::default() {
167            self.create_invitation(invitation).await
168        } else {
169            Err(AuthError::NotImplemented(
170                "Invitation creation overrides are not supported by this store".into(),
171            ))
172        }
173    }
174
175    /// Actual adapter page of pending rows, before expiry filtering.
176    async fn pending_invitation_page(
177        &self,
178        _org_id: &str,
179        _email: Option<&str>,
180    ) -> AuthResult<Vec<Invitation>> {
181        Err(AuthError::NotImplemented(
182            "Pending invitation pages are not supported by this store".into(),
183        ))
184    }
185    /// Update expiry independently; reissue retains every other invitation field.
186    async fn update_invitation_expiry(
187        &self,
188        _id: &str,
189        _expires_at: chrono::DateTime<chrono::Utc>,
190    ) -> AuthResult<Invitation> {
191        Err(AuthError::NotImplemented(
192            "Invitation expiry updates are not supported by this store".into(),
193        ))
194    }
195
196    async fn create_invitation(&self, invitation: CreateInvitation) -> AuthResult<Invitation>;
197    async fn get_invitation_by_id(&self, id: &str) -> AuthResult<Option<Invitation>>;
198    async fn get_pending_invitation(
199        &self,
200        org_id: &str,
201        email: &str,
202    ) -> AuthResult<Option<Invitation>>;
203    async fn update_invitation_status(
204        &self,
205        id: &str,
206        status: InvitationStatus,
207    ) -> AuthResult<Invitation>;
208    /// Atomically change only an invitation still at the expected status and return
209    /// the actual updated row. A mismatch or missing row returns None. This write
210    /// commits independently of a later membership transaction.
211    async fn update_invitation_status_if_status(
212        &self,
213        _id: &str,
214        _expected: InvitationStatus,
215        _status: InvitationStatus,
216    ) -> AuthResult<Option<Invitation>> {
217        Err(AuthError::NotImplemented(
218            "Conditional invitation updates are not supported by this store".into(),
219        ))
220    }
221    async fn list_organization_invitations(&self, org_id: &str) -> AuthResult<Vec<Invitation>>;
222    /// Count still-pending, unexpired invitations for an organization.
223    async fn count_pending_organization_invitations(&self, org_id: &str) -> AuthResult<i64>;
224    async fn list_user_invitations(&self, email: &str) -> AuthResult<Vec<Invitation>>;
225    /// Claim a pending invitation and persist memberships/session scope in one transition.
226    async fn accept_invitation_with_teams(
227        &self,
228        _invitation_id: &str,
229        _user_id: &str,
230        _session_token: &str,
231        _team_limits: &[(String, Option<f64>)],
232        _membership_limit: Option<usize>,
233    ) -> AuthResult<Option<(Invitation, Member)>> {
234        Err(AuthError::NotImplemented(
235            "Atomic invitation acceptance is not supported by this store".to_owned(),
236        ))
237    }
238    async fn update_invitation_team_ids(
239        &self,
240        _id: &str,
241        _team_ids: Option<String>,
242    ) -> AuthResult<Invitation> {
243        Err(AuthError::NotImplemented(
244            "Invitation team updates are not supported by this store".to_owned(),
245        ))
246    }
247}