Skip to main content

alibi_core/store/contracts/
organization.rs

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