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 active_column() -> &'static str;
153
154    fn expires_at_column() -> &'static str;
155
156    fn created_at_column() -> &'static str;
157
158    /// # Errors
159    ///
160    /// Returns an error if the identifier is invalid for this model's ID type.
161    fn parse_id(id: &str) -> AuthResult<SqlValue>;
162
163    /// # Errors
164    ///
165    /// Returns an error if the user identifier is invalid for this model's ID type.
166    fn parse_user_id(user_id: &str) -> AuthResult<SqlValue>;
167
168    fn new_active(
169        id: Option<SqlValue>,
170        token: String,
171        create_session: CreateSession,
172        now: DateTime<Utc>,
173    ) -> ActiveRow;
174
175    fn set_expires_at(active: &mut ActiveRow, expires_at: DateTime<Utc>);
176
177    fn set_updated_at(active: &mut ActiveRow, updated_at: DateTime<Utc>);
178
179    fn set_active_organization_id(active: &mut ActiveRow, organization_id: Option<String>);
180
181    /// # Errors
182    ///
183    /// Returns an error if the configured model does not support an active-team field.
184    fn set_active_team_id(_active: &mut ActiveRow, _team_id: Option<String>) -> AuthResult<()> {
185        Err(alibi_core::AuthError::internal(
186            "the session schema has no active-team field",
187        ))
188    }
189}
190
191pub trait SqlxAccountModel: AuthAccount + SqlxModel {
192    /// Bind configured fields to actual model columns while retaining raw numbers.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if configured additional fields cannot be bound to model columns.
197    fn additional_field_bindings(
198        fields: &FieldValues,
199        _backend: Engine,
200    ) -> AuthResult<Vec<(&'static str, SqlValue)>> {
201        if fields.is_empty() {
202            return Ok(Vec::new());
203        }
204        Err(alibi_core::AuthError::internal(
205            "the account schema has no additional field bindings",
206        ))
207    }
208
209    /// # Errors
210    ///
211    /// Returns an error if the field value is unsupported by the model column.
212    fn set_additional_field(
213        _active: &mut ActiveRow,
214        _column: &'static str,
215        _value: SqlValue,
216        _backend: Engine,
217    ) -> AuthResult<()> {
218        Err(alibi_core::AuthError::internal(
219            "the account schema cannot stage additional fields",
220        ))
221    }
222
223    /// Physical access, refresh and ID token columns for operator conversion.
224    /// Handwritten models opt in explicitly; absence fails closed.
225    fn oauth_token_columns() -> Option<[&'static str; 3]> {
226        None
227    }
228
229    fn id_column() -> &'static str;
230    fn provider_id_column() -> &'static str;
231    fn account_id_column() -> &'static str;
232    fn user_id_column() -> &'static str;
233    fn created_at_column() -> &'static str;
234    /// # Errors
235    ///
236    /// Returns an error if the identifier is invalid for this model's ID type.
237    fn parse_id(id: &str) -> AuthResult<SqlValue>;
238
239    /// # Errors
240    ///
241    /// Returns an error if the user identifier is invalid for this model's ID type.
242    fn parse_user_id(user_id: &str) -> AuthResult<SqlValue>;
243
244    fn new_active(
245        id: Option<SqlValue>,
246        create_account: CreateAccount,
247        now: DateTime<Utc>,
248    ) -> ActiveRow;
249    fn apply_update(active: &mut ActiveRow, update: UpdateAccount, now: DateTime<Utc>);
250}
251
252pub trait SqlxVerificationModel: AuthVerification + SqlxModel {
253    fn id_column() -> &'static str;
254    fn identifier_column() -> &'static str;
255    fn value_column() -> &'static str;
256    fn expires_at_column() -> &'static str;
257    fn created_at_column() -> &'static str;
258    #[must_use]
259    fn updated_at_column() -> Option<&'static str> {
260        None
261    }
262    /// # Errors
263    ///
264    /// Returns an error if the identifier is invalid for this model's ID type.
265    fn parse_id(id: &str) -> AuthResult<SqlValue>;
266
267    /// Convert a deterministic reservation key to this schema's ID type.
268    /// String schemas use the upstream key. UUID schemas can accept the UUID
269    /// derived from the same digest; numeric schemas must explicitly provide
270    /// a collision-resistant reservation strategy instead of truncating it.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if the reservation identifier is invalid for this model's ID type.
275    fn parse_reservation_id(encoded: &str, digest: [u8; 32]) -> AuthResult<SqlValue> {
276        if let Ok(id) = Self::parse_id(encoded) {
277            return Ok(id);
278        }
279        let mut bytes = [0; 16];
280        for (byte, source) in bytes.iter_mut().zip(digest) {
281            *byte = source;
282        }
283        Self::parse_id(&uuid::Uuid::from_bytes(bytes).to_string()).map_err(|_error| {
284            alibi_core::AuthError::internal(
285                "the verification schema cannot represent deterministic reservation IDs",
286            )
287        })
288    }
289
290    fn new_active(
291        id: Option<SqlValue>,
292        verification: CreateVerification,
293        now: DateTime<Utc>,
294    ) -> ActiveRow;
295}