Skip to main content

alibi_sqlx/store/
mod.rs

1//! `SQLx`-backed persistence implementation for built-in auth tables.
2
3mod accounts;
4mod api_key_usage_phases;
5mod api_keys;
6mod bundled_schema;
7mod device_codes;
8pub mod entities;
9mod invitations;
10mod jwks;
11mod members;
12pub(crate) mod migrator;
13mod oauth_clients;
14mod organization_roles;
15mod organizations;
16mod passkeys;
17mod sessions;
18mod teams;
19mod two_factor;
20mod users;
21mod verifications;
22mod wallets;
23
24#[doc(hidden)]
25pub mod __private_test_support {
26    pub mod bundled_schema {
27        pub use super::super::bundled_schema::BundledSchema;
28    }
29
30    pub mod migrator {
31        pub use super::super::migrator::run_migrations;
32    }
33}
34
35use crate::OrganizationModels;
36use crate::hooks::{DatabaseHooks, SqlxBackend, SqlxHookContext, current_request_hook_context};
37use crate::pool::{Engine, Exec, SqlxPool, SqlxTransaction};
38use crate::schema::{
39    AuthSchema, SqlxAccountModel, SqlxSessionModel, SqlxUserModel, SqlxVerificationModel,
40};
41use crate::sql::Sql;
42use alibi_core::config::AuthConfig;
43use alibi_core::error::AuthResult;
44use alibi_core::field_policy::FieldValues;
45use alibi_core::store::SchemaMigrator;
46use alibi_core::store::adapter::{AfterHook, AfterHookQueue};
47use alibi_core::store::{
48    AuthTransaction, BoxedTransactionValue, TransactionStore, TransactionWork,
49};
50use alibi_core::types::{AddTeamMemberResult, CreateJwk, Jwk, Member, Team};
51use alibi_core::user_validation::PreparedUserCreation;
52use alibi_core::verification::{
53    VerificationCreation, VerificationPublication, VerificationSnapshot,
54};
55use alibi_core::{
56    CreateAccount, CreateMember, CreatePasskey, CreateSession, CreateUser, CreateVerification,
57    Passkey,
58};
59use async_trait::async_trait;
60use chrono::{DateTime, Utc};
61use std::marker::PhantomData;
62use std::sync::Arc;
63
64#[derive(Clone)]
65pub struct SqlxStore<S: AuthSchema> {
66    config: Arc<AuthConfig>,
67    pool: SqlxPool,
68    hooks: Vec<Arc<dyn DatabaseHooks<S, SqlxBackend>>>,
69    organization_models: OrganizationModels,
70    _schema: PhantomData<S>,
71}
72
73impl<S: AuthSchema> std::fmt::Debug for SqlxStore<S> {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("SqlxStore").finish_non_exhaustive()
76    }
77}
78
79impl<S: AuthSchema> SqlxStore<S> {
80    #[must_use]
81    pub fn new(config: impl Into<Arc<AuthConfig>>, pool: impl Into<SqlxPool>) -> Self {
82        Self {
83            config: config.into(),
84            pool: pool.into(),
85            hooks: Vec::new(),
86            organization_models: crate::OrganizationModels::default(),
87            _schema: PhantomData,
88        }
89    }
90
91    /// Bind the organization plugin to application-owned tables.
92    #[must_use]
93    pub fn with_organization_models(mut self, models: OrganizationModels) -> Self {
94        self.organization_models = models;
95        self
96    }
97
98    #[must_use]
99    pub fn with_hooks(mut self, hooks: Vec<Arc<dyn DatabaseHooks<S, SqlxBackend>>>) -> Self {
100        self.hooks = hooks;
101        self
102    }
103
104    #[must_use]
105    pub fn hook<H: DatabaseHooks<S, SqlxBackend> + 'static>(mut self, hook: H) -> Self {
106        self.hooks.push(Arc::new(hook));
107        self
108    }
109
110    #[must_use]
111    pub const fn pool(&self) -> &SqlxPool {
112        &self.pool
113    }
114
115    #[must_use]
116    pub const fn config(&self) -> &Arc<AuthConfig> {
117        &self.config
118    }
119
120    pub(crate) fn hooks(&self) -> &[Arc<dyn DatabaseHooks<S, SqlxBackend>>] {
121        &self.hooks
122    }
123
124    pub(crate) async fn generated_id(
125        &self,
126        exec: Exec<'_>,
127        model: &str,
128        table: &str,
129        column: &str,
130    ) -> AuthResult<Option<String>> {
131        let policy = &self.config.advanced.database;
132        if !policy.serial_ids() {
133            return policy.generated_id(model);
134        }
135        let [ddl, allocate] = alibi_core::config::serial_id_statements(
136            table,
137            column,
138            exec.engine() == Engine::Postgres,
139        );
140        _ = exec.execute(Sql::with(exec.engine(), &ddl)).await?;
141        let value = exec
142            .fetch_scalar::<i64>(Sql::with(exec.engine(), &allocate))
143            .await?
144            .ok_or_else(|| alibi_core::AuthError::internal("ID allocation returned no value"))?;
145        Ok(Some(value.to_string()))
146    }
147
148    async fn begin(&self, immediate: bool) -> AuthResult<SqlxTransaction> {
149        let mut transaction = self.pool.begin(immediate).await?;
150        transaction.config = Some(self.config.clone());
151        Ok(transaction)
152    }
153
154    pub(crate) fn exec(&self) -> Exec<'_> {
155        Exec::pool(&self.pool).with_config(self.config.as_ref())
156    }
157
158    pub(crate) fn hook_context<'a>(
159        &'a self,
160        tx: Option<&'a SqlxTransaction>,
161    ) -> SqlxHookContext<'a> {
162        SqlxHookContext {
163            config: self.config.as_ref(),
164            db: &self.pool,
165            tx,
166            request: current_request_hook_context(),
167        }
168    }
169
170    pub(crate) fn find_many_limit(&self) -> i64 {
171        // Mirrors the unsigned page binding of the reference adapter.
172        i64::try_from(self.config.advanced.database.default_find_many_limit).unwrap_or(i64::MAX)
173    }
174
175    /// Run work on a fresh transaction, committing on success. On failure the
176    /// transaction is dropped, which rolls it back, and the work's error returns.
177    pub(crate) async fn in_transaction<T>(
178        &self,
179        immediate: bool,
180        work: impl AsyncFnOnce(&SqlxTransaction) -> AuthResult<T>,
181    ) -> AuthResult<T> {
182        let transaction = self.begin(immediate).await?;
183        let value = work(&transaction).await?;
184        transaction.commit().await?;
185        Ok(value)
186    }
187}
188
189/// Installs the bundled auth schema in `better_auth_migrations`, the ledger
190/// the `SeaORM` adapter also records.
191#[async_trait]
192impl<S: AuthSchema> SchemaMigrator for SqlxStore<S> {
193    async fn migrate(&self) -> AuthResult<()> {
194        migrator::run_migrations_scoped(&self.pool, &self.config).await?;
195        if self.config.advanced.database.serial_ids() {
196            let [ddl, _] = alibi_core::config::serial_id_statements("", "", false);
197            let exec = self.exec();
198            _ = exec.execute(Sql::with(exec.engine(), &ddl)).await?;
199        }
200        if let Some(mapping) = &self.config.advanced.database.two_factor {
201            let commands = mapping.migration_statements()?;
202            self.in_transaction(false, async move |tx| {
203                let exec = Exec::tx(tx);
204                if migrator::has_table(exec, "twoFactor").await? {
205                    for command in commands {
206                        _ = exec.execute(Sql::with(exec.engine(), &command)).await?;
207                    }
208                }
209                Ok(())
210            })
211            .await?;
212        }
213        if let Some(mapping) = &self.config.advanced.database.sso_provider {
214            let commands = mapping.migration_statements()?;
215            self.in_transaction(false, async move |tx| {
216                let exec = Exec::tx(tx);
217                if migrator::has_table(exec, "ssoProvider").await? {
218                    for command in commands {
219                        _ = exec.execute(Sql::with(exec.engine(), &command)).await?;
220                    }
221                }
222                Ok(())
223            })
224            .await?;
225        }
226
227        let storage = &self.config.advanced.database;
228        alibi_core::store::validate_sso_fields(storage)?;
229        if !storage.sso_provider_fields.0.is_empty() {
230            let table = storage
231                .sso_provider
232                .as_ref()
233                .map_or("ssoProvider", |mapping| mapping.table_name.as_str());
234            self.in_transaction(false, async move |tx| {
235                let exec = Exec::tx(tx);
236                for (name, field) in &storage.sso_provider_fields.0 {
237                    let column = alibi_core::store::sso_field_column(name, field);
238                    if !migrator::has_column(exec, table, column).await? {
239                        let definition = alibi_core::store::sso_field_definition(
240                            name,
241                            field,
242                            exec.engine() == crate::pool::Engine::Postgres,
243                        )?;
244                        let mut ddl = Sql::with(exec.engine(), "ALTER TABLE ");
245                        ddl.ident(table);
246                        ddl.push(" ADD COLUMN ");
247                        ddl.push(&definition);
248                        _ = exec.execute(ddl).await?;
249                    }
250                }
251                Ok(())
252            })
253            .await?;
254        }
255
256        Ok(())
257    }
258}
259
260struct SqlxStoreTransaction<'a, S: AuthSchema> {
261    store: &'a SqlxStore<S>,
262    tx: &'a SqlxTransaction,
263    pending_after: AfterHookQueue<S>,
264}
265
266#[async_trait]
267impl<S> AuthTransaction<S> for SqlxStoreTransaction<'_, S>
268where
269    S: AuthSchema,
270    S::User: SqlxUserModel,
271    S::Account: SqlxAccountModel,
272    S::Session: SqlxSessionModel,
273    S::Verification: SqlxVerificationModel,
274{
275    async fn get_scim_connection_binding(
276        &self,
277        id: &str,
278    ) -> AuthResult<Option<alibi_core::store::ScimConnectionBinding>> {
279        self.store
280            .get_scim_connection_binding_with_exec(Exec::tx(self.tx), id)
281            .await
282    }
283    async fn scim_projection_candidates(
284        &self,
285        domain: &str,
286        cursor: Option<&str>,
287    ) -> AuthResult<Vec<alibi_core::store::ScimProjectionCandidate>> {
288        self.store
289            .scim_projection_candidates_with_exec(Exec::tx(self.tx), domain, cursor)
290            .await
291    }
292    async fn renew_scim_decommission_lease(
293        &self,
294        binding: &alibi_core::store::ScimConnectionBinding,
295        lease: &str,
296    ) -> AuthResult<bool> {
297        self.store
298            .renew_scim_decommission_lease_with_exec(Exec::tx(self.tx), binding, lease)
299            .await
300    }
301    async fn advance_scim_decommission(
302        &self,
303        binding: &alibi_core::store::ScimConnectionBinding,
304        lease: &str,
305        cursor: Option<&str>,
306        count: usize,
307        more: bool,
308    ) -> AuthResult<Option<alibi_core::store::ScimConnectionBinding>> {
309        self.store
310            .advance_scim_decommission_with_exec(
311                Exec::tx(self.tx),
312                binding,
313                lease,
314                cursor,
315                count,
316                more,
317            )
318            .await
319    }
320    async fn reconcile_scim_identity(
321        &self,
322        user_id: &str,
323    ) -> AuthResult<alibi_core::store::ScimIdentityState> {
324        self.store
325            .reconcile_scim_identity_with_exec(Exec::tx(self.tx), user_id)
326            .await
327    }
328    async fn delete_user_sessions(&self, user_id: &str) -> AuthResult<()> {
329        use crate::model::SqlxModel;
330        let exec = Exec::tx(self.tx);
331        let table = <S::Session as SqlxModel>::TABLE;
332        let id = <S::Session as SqlxSessionModel>::parse_user_id(user_id)?;
333        let mut sql = Sql::with(exec.engine(), "DELETE FROM ");
334        sql.ident(table);
335        sql.push(" WHERE ");
336        sql.compare_model::<S::Session>(table, S::Session::user_id_column(), " = ", id);
337        _ = exec.execute(sql).await?;
338        Ok(())
339    }
340    async fn check_scim_user_attributes(
341        &self,
342        id: &str,
343        connection: &str,
344        attributes: &alibi_core::store::ScimUserAttributes,
345    ) -> AuthResult<()> {
346        self.store
347            .assert_scim_user_keys_with_exec(
348                Exec::tx(self.tx),
349                connection,
350                &attributes.user_name_key,
351                attributes.external_id_key.as_deref(),
352                None,
353                Some(id),
354            )
355            .await
356    }
357    async fn lock_scim_user(
358        &self,
359        id: &str,
360        connection: &str,
361        domain: &str,
362        attributes: Option<&alibi_core::store::ScimUserAttributes>,
363        now: chrono::DateTime<chrono::Utc>,
364    ) -> AuthResult<alibi_core::store::ScimUserMutationContext> {
365        self.store
366            .lock_scim_user_with_exec(Exec::tx(self.tx), id, connection, domain, attributes, now)
367            .await
368    }
369    async fn replace_scim_user(
370        &self,
371        id: &str,
372        data: alibi_core::store::ReplaceScimUser,
373        roles: bool,
374    ) -> AuthResult<(
375        alibi_core::store::ScimUserRecord,
376        Vec<alibi_core::store::ScimProjectionPreparedUser>,
377    )> {
378        self.store
379            .replace_scim_user_with_exec(Exec::tx(self.tx), id, data, roles)
380            .await
381    }
382    async fn create_scim_user(
383        &self,
384        data: alibi_core::store::CreateScimUser,
385        roles: bool,
386    ) -> AuthResult<(
387        alibi_core::store::ScimUserRecord,
388        Vec<alibi_core::store::ScimProjectionPreparedUser>,
389    )> {
390        self.store
391            .create_scim_user_with_exec(Exec::tx(self.tx), data, roles)
392            .await
393    }
394    async fn delete_scim_user(
395        &self,
396        id: &str,
397        connection: &str,
398        domain: &str,
399        roles: bool,
400    ) -> AuthResult<(String, Vec<alibi_core::store::ScimProjectionPreparedUser>)> {
401        self.store
402            .delete_scim_user_with_exec(Exec::tx(self.tx), id, connection, domain, roles)
403            .await
404    }
405    async fn patch_scim_group(
406        &self,
407        id: &str,
408        connection: &str,
409        domain: &str,
410        patch: alibi_core::store::ScimGroupPatch,
411        roles: bool,
412    ) -> AuthResult<(
413        alibi_core::store::ScimGroupRecord,
414        Vec<alibi_core::store::ScimProjectionPreparedUser>,
415    )> {
416        self.store
417            .patch_scim_group_with_exec(Exec::tx(self.tx), id, connection, domain, patch, roles)
418            .await
419    }
420    async fn replace_scim_group(
421        &self,
422        id: &str,
423        data: alibi_core::store::ReplaceScimGroup,
424        roles: bool,
425    ) -> AuthResult<(
426        alibi_core::store::ScimGroupRecord,
427        Vec<alibi_core::store::ScimProjectionPreparedUser>,
428    )> {
429        self.store
430            .replace_scim_group_with_exec(Exec::tx(self.tx), id, data, roles)
431            .await
432    }
433    async fn delete_scim_group(
434        &self,
435        id: &str,
436        connection: &str,
437        domain: &str,
438        roles: bool,
439    ) -> AuthResult<Vec<alibi_core::store::ScimProjectionPreparedUser>> {
440        self.store
441            .delete_scim_group_with_exec(Exec::tx(self.tx), id, connection, domain, roles)
442            .await
443    }
444    async fn create_scim_group(
445        &self,
446        data: alibi_core::store::CreateScimGroup,
447        roles: bool,
448    ) -> AuthResult<(
449        alibi_core::store::ScimGroupRecord,
450        Vec<alibi_core::store::ScimProjectionPreparedUser>,
451    )> {
452        self.store
453            .create_scim_group_with_exec(Exec::tx(self.tx), data, roles)
454            .await
455    }
456    async fn fence_scim_connection(&self, id: &str, key: &str) -> AuthResult<()> {
457        self.store
458            .fence_scim_connection_with_exec(Exec::tx(self.tx), id, key)
459            .await
460    }
461    async fn prepare_scim_projection(
462        &self,
463        domain: &str,
464        ids: &[String],
465        roles: bool,
466    ) -> AuthResult<Vec<alibi_core::store::ScimProjectionPreparedUser>> {
467        self.store
468            .prepare_scim_projection_with_exec(Exec::tx(self.tx), domain, ids, roles)
469            .await
470    }
471    async fn create_scim_projection_grant(
472        &self,
473        data: alibi_core::store::CreateScimProjectionGrant,
474    ) -> AuthResult<()> {
475        self.store
476            .create_scim_projection_grant_with_exec(Exec::tx(self.tx), data)
477            .await
478    }
479    async fn remove_scim_projection_grants(
480        &self,
481        domain: &str,
482        user_id: &str,
483        ids: &[String],
484    ) -> AuthResult<()> {
485        self.store
486            .remove_scim_projection_grants_with_exec(Exec::tx(self.tx), domain, user_id, ids)
487            .await
488    }
489    async fn get_user_by_email(&self, email: &str) -> AuthResult<Option<S::User>> {
490        self.store
491            .get_user_by_email_with_connection(Exec::tx(self.tx), email)
492            .await
493    }
494    async fn get_account(
495        &self,
496        provider: &str,
497        account_id: &str,
498    ) -> AuthResult<Option<S::Account>> {
499        self.store
500            .get_account_with_connection(Exec::tx(self.tx), provider, account_id)
501            .await
502    }
503    async fn update_user(&self, id: &str, update: alibi_core::UpdateUser) -> AuthResult<S::User> {
504        let user = self
505            .store
506            .update_user_with_connection(Exec::tx(self.tx), Some(self.tx), id, update)
507            .await?;
508        self.pending_after
509            .push(AfterHook::UserUpdated(user.clone()))
510            .await;
511        Ok(user)
512    }
513    async fn update_account(
514        &self,
515        id: &str,
516        update: alibi_core::UpdateAccount,
517    ) -> AuthResult<S::Account> {
518        let account = self
519            .store
520            .update_account_with_connection(Exec::tx(self.tx), Some(self.tx), id, update)
521            .await?;
522        self.pending_after
523            .push(AfterHook::AccountUpdated(account.clone()))
524            .await;
525        Ok(account)
526    }
527    async fn lock_sso_provider(
528        &self,
529        id: &str,
530        provider_id: &str,
531    ) -> AuthResult<Option<alibi_core::store::SsoProvider>> {
532        self.store
533            .lock_sso_provider_with(Exec::tx(self.tx), id, provider_id)
534            .await
535    }
536    async fn update_sso_provider(
537        &self,
538        update: alibi_core::store::SsoProviderUpdate,
539    ) -> AuthResult<Option<alibi_core::store::SsoProvider>> {
540        self.store
541            .update_sso_provider_with(Exec::tx(self.tx), update)
542            .await
543    }
544    async fn delete_sso_provider_and_accounts(
545        &self,
546        id: &str,
547        provider_id: &str,
548    ) -> AuthResult<bool> {
549        self.store
550            .delete_sso_provider_with(Exec::tx(self.tx), id, provider_id)
551            .await
552    }
553    async fn list_jwks(&self) -> AuthResult<Vec<Jwk>> {
554        self.store.list_jwks_with(Exec::tx(self.tx)).await
555    }
556    async fn get_jwk_by_id(&self, id: &str) -> AuthResult<Option<Jwk>> {
557        self.store.get_jwk_with(Exec::tx(self.tx), id).await
558    }
559    async fn create_jwk(&self, data: CreateJwk) -> AuthResult<Jwk> {
560        self.store.create_jwk_with(Exec::tx(self.tx), data).await
561    }
562
563    async fn get_team(&self, organization_id: &str, team_id: &str) -> AuthResult<Option<Team>> {
564        self.store
565            .get_team_with_connection(Exec::tx(self.tx), Some(organization_id), team_id)
566            .await
567    }
568    async fn add_team_member(
569        &self,
570        team_id: &str,
571        user_id: &str,
572        maximum: Option<f64>,
573    ) -> AuthResult<AddTeamMemberResult> {
574        self.store
575            .add_team_member_in_tx(self.tx, team_id, user_id, maximum)
576            .await
577    }
578    async fn create_member(&self, member: CreateMember) -> AuthResult<Member> {
579        self.store
580            .create_member_with_connection(Exec::tx(self.tx), member)
581            .await
582    }
583    async fn update_session_active_team(
584        &self,
585        token: &str,
586        team_id: Option<&str>,
587    ) -> AuthResult<S::Session> {
588        self.store
589            .update_session_scope_with_connection(
590                Exec::tx(self.tx),
591                token,
592                sessions::SessionScope::Team(team_id),
593            )
594            .await
595    }
596    async fn update_session_active_organization(
597        &self,
598        token: &str,
599        organization_id: Option<&str>,
600    ) -> AuthResult<S::Session> {
601        self.store
602            .update_session_scope_with_connection(
603                Exec::tx(self.tx),
604                token,
605                sessions::SessionScope::Organization(organization_id),
606            )
607            .await
608    }
609    async fn provider_verification_output(
610        &self,
611        id: &str,
612    ) -> AuthResult<Option<serde_json::Value>> {
613        users::provider_verification_output::<S::User>(Exec::tx(self.tx), id).await
614    }
615    async fn get_user_by_id(&self, id: &str) -> AuthResult<Option<S::User>> {
616        users::find_user_by_id::<S::User>(Exec::tx(self.tx), id, users::Lock::None).await
617    }
618    async fn create_passkey(&self, data: CreatePasskey) -> AuthResult<Passkey> {
619        self.store
620            .create_passkey_with_connection(Exec::tx(self.tx), data)
621            .await
622    }
623    async fn create_user(&self, create_user: CreateUser) -> AuthResult<S::User> {
624        let user = self.store.create_user_in_tx(self.tx, create_user).await?;
625        self.pending_after
626            .push(AfterHook::UserCreated(user.clone()))
627            .await;
628        Ok(user)
629    }
630
631    async fn create_user_prepared(&self, prepared: PreparedUserCreation) -> AuthResult<S::User> {
632        let user = self
633            .store
634            .create_user_prepared_in_tx(self.tx, prepared)
635            .await?;
636        self.pending_after
637            .push(AfterHook::UserCreated(user.clone()))
638            .await;
639        Ok(user)
640    }
641
642    async fn create_account(&self, create_account: CreateAccount) -> AuthResult<S::Account> {
643        let account = self
644            .store
645            .create_account_in_tx(self.tx, create_account)
646            .await?;
647        self.pending_after
648            .push(AfterHook::AccountCreated(account.clone()))
649            .await;
650        Ok(account)
651    }
652
653    async fn prepare_secondary_session_update(
654        &self,
655        session: S::Session,
656        expires_at: Option<DateTime<Utc>>,
657        fields: FieldValues,
658    ) -> AuthResult<Option<(S::Session, FieldValues)>> {
659        self.store
660            .prepare_secondary_update_with_connection(
661                Exec::tx(self.tx),
662                Some(self.tx),
663                session,
664                expires_at,
665                fields,
666            )
667            .await
668    }
669    async fn complete_secondary_session_update(
670        &self,
671        session: S::Session,
672        expires_at: Option<DateTime<Utc>>,
673        fields: FieldValues,
674        persist: bool,
675    ) -> AuthResult<Option<S::Session>> {
676        use alibi_core::AuthSession;
677        let token = session.token().to_owned();
678        let result = self
679            .store
680            .complete_secondary_update_with_connection(
681                Exec::tx(self.tx),
682                Some(self.tx),
683                session,
684                expires_at,
685                fields,
686                persist,
687            )
688            .await?;
689        let event = result.as_ref().map_or_else(
690            || AfterHook::SessionUpdateMissing(token),
691            |model| AfterHook::SessionUpdated(model.clone()),
692        );
693        self.pending_after.push(event).await;
694        Ok(result)
695    }
696    async fn prepare_secondary_session_creation(
697        &self,
698        input: CreateSession,
699        persist: bool,
700    ) -> AuthResult<S::Session> {
701        let session = self
702            .store
703            .prepare_secondary_session_in_tx(self.tx, input, persist)
704            .await?;
705        self.pending_after
706            .push(AfterHook::SessionCreated(session.clone()))
707            .await;
708        Ok(session)
709    }
710    async fn create_session(&self, create_session: CreateSession) -> AuthResult<S::Session> {
711        let session = self
712            .store
713            .create_session_in_tx(self.tx, create_session)
714            .await?;
715        self.pending_after
716            .push(AfterHook::SessionCreated(session.clone()))
717            .await;
718        Ok(session)
719    }
720    async fn create_verification_record(
721        &self,
722        data: VerificationCreation,
723        publication: VerificationPublication,
724    ) -> AuthResult<Option<VerificationSnapshot>> {
725        let snapshot = self
726            .store
727            .create_verification_record_with_connection(
728                Exec::tx(self.tx),
729                Some(self.tx),
730                data,
731                publication,
732            )
733            .await?;
734        if let Some(snapshot) = &snapshot {
735            self.pending_after
736                .push(AfterHook::VerificationRecordCreated(snapshot.clone()))
737                .await;
738        }
739        Ok(snapshot)
740    }
741
742    async fn create_verification(
743        &self,
744        verification: CreateVerification,
745    ) -> AuthResult<S::Verification> {
746        let verification = self
747            .store
748            .create_verification_in_tx(self.tx, verification)
749            .await?;
750        self.pending_after
751            .push(AfterHook::VerificationCreated(verification.clone()))
752            .await;
753        Ok(verification)
754    }
755}
756
757#[async_trait]
758impl<S> TransactionStore<S> for SqlxStore<S>
759where
760    S: AuthSchema,
761    S::User: SqlxUserModel,
762    S::Account: SqlxAccountModel,
763    S::Session: SqlxSessionModel,
764    S::Verification: SqlxVerificationModel,
765{
766    async fn transaction_boxed(
767        &self,
768        work: Box<TransactionWork<S>>,
769    ) -> AuthResult<BoxedTransactionValue> {
770        self.transaction_boxed_with_policy(
771            work,
772            alibi_core::store::AfterCommitErrorPolicy::Propagate,
773        )
774        .await
775    }
776    async fn transaction_boxed_with_policy(
777        &self,
778        work: Box<TransactionWork<S>>,
779        policy: alibi_core::store::AfterCommitErrorPolicy,
780    ) -> AuthResult<BoxedTransactionValue> {
781        let tx = self.begin(false).await?;
782        let tx_store = SqlxStoreTransaction {
783            store: self,
784            tx: &tx,
785            pending_after: AfterHookQueue::new(),
786        };
787        let outcome = work(&tx_store).await;
788        let pending_after = tx_store.pending_after;
789        match outcome {
790            Ok(value) => {
791                tx.commit().await?;
792                // After hooks run only once the auth writes are durable, in
793                // operation order; rollback drops them.
794                pending_after
795                    .run_with_policy(self.hooks(), &self.hook_context(None), policy)
796                    .await?;
797                Ok(value)
798            }
799            Err(err) => {
800                tx.rollback().await?;
801                Err(err)
802            }
803        }
804    }
805}
806
807/// `FOR UPDATE` on PostgreSQL; SQLite relies on its `BEGIN IMMEDIATE` writer lock.
808pub(crate) fn lock_exclusive(sql: &mut Sql) {
809    if sql.engine() == Engine::Postgres {
810        sql.push(" FOR UPDATE");
811    }
812}
813
814/// `FOR SHARE` on PostgreSQL; SQLite relies on its `BEGIN IMMEDIATE` writer lock.
815pub(crate) fn lock_shared(sql: &mut Sql) {
816    if sql.engine() == Engine::Postgres {
817        sql.push(" FOR SHARE");
818    }
819}
820
821/// Bind raw numeric `LIMIT`/`OFFSET` values; the database validates them.
822pub(crate) fn bind_page(sql: &mut crate::sql::Sql, limit: Option<f64>, offset: Option<f64>) {
823    for (clause, number) in [(" LIMIT ", limit), (" OFFSET ", offset)] {
824        if let Some(number) = number {
825            sql.push(clause);
826            if sql.engine() == Engine::Postgres {
827                // node-postgres sends Number parameters as text. Binding float8
828                // instead lets PostgreSQL round fractional pages to bigint.
829                sql.bind(ryu_js::Buffer::new().format(number).to_owned());
830                sql.push("::bigint");
831            } else {
832                sql.bind(number);
833            }
834        }
835    }
836}
837
838mod sso;
839
840mod scim;
841mod scim_decommission;
842mod scim_groups;
843mod scim_projection;
844mod scim_user_create;
845mod scim_user_replace;
846mod scim_users;