Skip to main content

alibi_sqlx/
schema.rs

1//! `SQLx` model bindings for Alibi schemas.
2
3use crate::model::{ActiveRow, SqlxModel};
4use crate::pool::Engine;
5use crate::value::SqlValue;
6use alibi_core::AuthError;
7use alibi_core::entity::{AuthAccount, AuthSession, AuthUser, AuthVerification};
8use alibi_core::error::AuthResult;
9use alibi_core::field_policy::FieldValues;
10pub use alibi_core::schema::AuthSchema;
11use alibi_core::types::{
12    CreateAccount, CreateSession, CreateUser, CreateVerification, UpdateAccount, UpdateUser,
13};
14use chrono::{DateTime, Utc};
15
16pub trait SqlxUserModel: AuthUser + SqlxModel {
17    /// Bind configured fields to actual model columns while retaining raw numbers.
18    ///
19    /// # Errors
20    ///
21    /// Returns an error if configured additional fields cannot be bound to model columns.
22    fn additional_field_bindings(
23        fields: &FieldValues,
24        _backend: Engine,
25    ) -> AuthResult<Vec<(&'static str, SqlValue)>> {
26        if fields.is_empty() {
27            return Ok(Vec::new());
28        }
29        Err(alibi_core::AuthError::internal(
30            "the user schema has no additional field bindings",
31        ))
32    }
33
34    /// # Errors
35    ///
36    /// Returns an error if the field value is unsupported by the model column.
37    fn set_additional_field(
38        _active: &mut ActiveRow,
39        _column: &'static str,
40        _value: SqlValue,
41        _backend: Engine,
42    ) -> AuthResult<()> {
43        Err(alibi_core::AuthError::internal(
44            "the user schema cannot stage additional fields",
45        ))
46    }
47
48    fn id_column() -> &'static str;
49
50    fn email_column() -> &'static str;
51
52    /// Returns the username column, or `None` if the username plugin is not enabled.
53    #[must_use]
54    fn username_column() -> Option<&'static str> {
55        None
56    }
57
58    #[must_use]
59    fn phone_number_column() -> Option<&'static str> {
60        None
61    }
62
63    fn name_column() -> &'static str;
64
65    fn created_at_column() -> &'static str;
66
67    /// Bind an admin user-list field to its actual model column. Derived models
68    /// provide all declared fields, including renamed physical columns. Manual
69    /// models may add their plugin/application field bindings explicitly.
70    #[must_use]
71    fn list_users_column(field: &str) -> Option<&'static str> {
72        match field {
73            "id" => Some(Self::id_column()),
74            "email" => Some(Self::email_column()),
75            "name" => Some(Self::name_column()),
76            "username" => Self::username_column(),
77            "createdAt" => Some(Self::created_at_column()),
78            _ => None,
79        }
80    }
81
82    /// # Errors
83    ///
84    /// Returns an error if the identifier is invalid for this model's ID type.
85    fn parse_id(id: &str) -> AuthResult<SqlValue>;
86
87    fn new_active(id: Option<SqlValue>, create_user: CreateUser, now: DateTime<Utc>) -> ActiveRow;
88
89    fn apply_update(active: &mut ActiveRow, update: UpdateUser, now: DateTime<Utc>);
90
91    /// Prepare JSON bindings after application hooks and before the atomic write.
92    /// Manual model implementations may retain their existing binding behavior.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if metadata preparation fails.
97    fn prepare_json_metadata(_active: &mut ActiveRow, _backend: Engine) -> AuthResult<()> {
98        Ok(())
99    }
100}
101
102pub trait SqlxSessionModel: AuthSession + SqlxModel {
103    /// Materialize the application model without a database insert.
104    /// Custom schemas opt in to secondary-only sessions by implementing this binding.
105    ///
106    /// # Errors
107    ///
108    /// Returns `NotImplemented` unless the schema supports secondary sessions.
109    fn materialize_secondary(_active: ActiveRow) -> AuthResult<Self> {
110        Err(AuthError::NotImplemented(
111            "Secondary session materialization is unsupported".into(),
112        ))
113    }
114
115    /// Bind configured fields to actual model columns while retaining raw numbers.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if configured additional fields cannot be bound to model columns.
120    fn additional_field_bindings(
121        fields: &FieldValues,
122        _backend: Engine,
123    ) -> AuthResult<Vec<(&'static str, SqlValue)>> {
124        if fields.is_empty() {
125            return Ok(Vec::new());
126        }
127        Err(alibi_core::AuthError::internal(
128            "the session schema has no additional field bindings",
129        ))
130    }
131
132    /// # Errors
133    ///
134    /// Returns an error if the field value is unsupported by the model column.
135    fn set_additional_field(
136        _active: &mut ActiveRow,
137        _column: &'static str,
138        _value: SqlValue,
139        _backend: Engine,
140    ) -> AuthResult<()> {
141        Err(alibi_core::AuthError::internal(
142            "the session schema cannot stage additional fields",
143        ))
144    }
145
146    fn id_column() -> &'static str;
147
148    fn token_column() -> &'static str;
149
150    fn user_id_column() -> &'static str;
151
152    fn expires_at_column() -> &'static str;
153
154    fn created_at_column() -> &'static str;
155
156    /// # Errors
157    ///
158    /// Returns an error if the identifier is invalid for this model's ID type.
159    fn parse_id(id: &str) -> AuthResult<SqlValue>;
160
161    /// # Errors
162    ///
163    /// Returns an error if the user identifier is invalid for this model's ID type.
164    fn parse_user_id(user_id: &str) -> AuthResult<SqlValue>;
165
166    fn new_active(
167        id: Option<SqlValue>,
168        token: String,
169        create_session: CreateSession,
170        now: DateTime<Utc>,
171    ) -> ActiveRow;
172
173    fn set_expires_at(active: &mut ActiveRow, expires_at: DateTime<Utc>);
174
175    fn set_updated_at(active: &mut ActiveRow, updated_at: DateTime<Utc>);
176
177    fn set_active_organization_id(active: &mut ActiveRow, organization_id: Option<String>);
178
179    /// # Errors
180    ///
181    /// Returns an error if the configured model does not support an active-team field.
182    fn set_active_team_id(_active: &mut ActiveRow, _team_id: Option<String>) -> AuthResult<()> {
183        Err(alibi_core::AuthError::internal(
184            "the session schema has no active-team field",
185        ))
186    }
187}
188
189pub trait SqlxAccountModel: AuthAccount + SqlxModel {
190    /// Bind configured fields to actual model columns while retaining raw numbers.
191    ///
192    /// # Errors
193    ///
194    /// Returns an error if configured additional fields cannot be bound to model columns.
195    fn additional_field_bindings(
196        fields: &FieldValues,
197        _backend: Engine,
198    ) -> AuthResult<Vec<(&'static str, SqlValue)>> {
199        if fields.is_empty() {
200            return Ok(Vec::new());
201        }
202        Err(alibi_core::AuthError::internal(
203            "the account schema has no additional field bindings",
204        ))
205    }
206
207    /// # Errors
208    ///
209    /// Returns an error if the field value is unsupported by the model column.
210    fn set_additional_field(
211        _active: &mut ActiveRow,
212        _column: &'static str,
213        _value: SqlValue,
214        _backend: Engine,
215    ) -> AuthResult<()> {
216        Err(alibi_core::AuthError::internal(
217            "the account schema cannot stage additional fields",
218        ))
219    }
220
221    fn id_column() -> &'static str;
222    fn provider_id_column() -> &'static str;
223    fn account_id_column() -> &'static str;
224    fn user_id_column() -> &'static str;
225    fn created_at_column() -> &'static str;
226    /// # Errors
227    ///
228    /// Returns an error if the identifier is invalid for this model's ID type.
229    fn parse_id(id: &str) -> AuthResult<SqlValue>;
230
231    /// # Errors
232    ///
233    /// Returns an error if the user identifier is invalid for this model's ID type.
234    fn parse_user_id(user_id: &str) -> AuthResult<SqlValue>;
235
236    fn new_active(
237        id: Option<SqlValue>,
238        create_account: CreateAccount,
239        now: DateTime<Utc>,
240    ) -> ActiveRow;
241    fn apply_update(active: &mut ActiveRow, update: UpdateAccount, now: DateTime<Utc>);
242}
243
244pub trait SqlxVerificationModel: AuthVerification + SqlxModel {
245    fn id_column() -> &'static str;
246    fn identifier_column() -> &'static str;
247    fn value_column() -> &'static str;
248    fn expires_at_column() -> &'static str;
249    fn created_at_column() -> &'static str;
250    #[must_use]
251    fn updated_at_column() -> Option<&'static str> {
252        None
253    }
254    /// # Errors
255    ///
256    /// Returns an error if the identifier is invalid for this model's ID type.
257    fn parse_id(id: &str) -> AuthResult<SqlValue>;
258
259    /// Convert a deterministic reservation key to this schema's ID type.
260    /// String schemas use the upstream key. UUID schemas can accept the UUID
261    /// derived from the same digest; numeric schemas must explicitly provide
262    /// a collision-resistant reservation strategy instead of truncating it.
263    ///
264    /// # Errors
265    ///
266    /// Returns an error if the reservation identifier is invalid for this model's ID type.
267    fn parse_reservation_id(encoded: &str, digest: [u8; 32]) -> AuthResult<SqlValue> {
268        if let Ok(id) = Self::parse_id(encoded) {
269            return Ok(id);
270        }
271        let mut bytes = [0; 16];
272        for (byte, source) in bytes.iter_mut().zip(digest) {
273            *byte = source;
274        }
275        Self::parse_id(&uuid::Uuid::from_bytes(bytes).to_string()).map_err(|_error| {
276            alibi_core::AuthError::internal(
277                "the verification schema cannot represent deterministic reservation IDs",
278            )
279        })
280    }
281
282    fn new_active(
283        id: Option<SqlValue>,
284        verification: CreateVerification,
285        now: DateTime<Utc>,
286    ) -> ActiveRow;
287}