1mod 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 #[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 i64::try_from(self.config.advanced.database.default_find_many_limit).unwrap_or(i64::MAX)
173 }
174
175 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#[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 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
807pub(crate) fn lock_exclusive(sql: &mut Sql) {
809 if sql.engine() == Engine::Postgres {
810 sql.push(" FOR UPDATE");
811 }
812}
813
814pub(crate) fn lock_shared(sql: &mut Sql) {
816 if sql.engine() == Engine::Postgres {
817 sql.push(" FOR SHARE");
818 }
819}
820
821pub(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 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;