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::entity::{AuthAccount, AuthSession, AuthUser, AuthVerification};
7use alibi_core::error::AuthResult;
8pub use alibi_core::schema::AuthSchema;
9use alibi_core::types::{
10    CreateAccount, CreateSession, CreateUser, CreateVerification, UpdateAccount, UpdateUser,
11};
12use chrono::{DateTime, Utc};
13
14pub trait SqlxUserModel: AuthUser + SqlxModel {
15    /// Bind configured fields to actual model columns while retaining raw numbers.
16    ///
17    /// # Errors
18    ///
19    /// Returns an error if configured additional fields cannot be bound to model columns.
20    fn additional_field_bindings(
21        fields: &alibi_core::field_policy::FieldValues,
22        _backend: Engine,
23    ) -> AuthResult<Vec<(&'static str, SqlValue)>> {
24        if fields.is_empty() {
25            return Ok(Vec::new());
26        }
27        Err(alibi_core::AuthError::internal(
28            "the user schema has no additional field bindings",
29        ))
30    }
31
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if the field value is unsupported by the model column.
36    fn set_additional_field(
37        _active: &mut ActiveRow,
38        _column: &'static str,
39        _value: SqlValue,
40        _backend: Engine,
41    ) -> AuthResult<()> {
42        Err(alibi_core::AuthError::internal(
43            "the user schema cannot stage additional fields",
44        ))
45    }
46
47    fn id_column() -> &'static str;
48
49    fn email_column() -> &'static str;
50
51    /// Returns the username column, or `None` if the username plugin is not enabled.
52    #[must_use]
53    fn username_column() -> Option<&'static str> {
54        None
55    }
56
57    #[must_use]
58    fn phone_number_column() -> Option<&'static str> {
59        None
60    }
61
62    fn name_column() -> &'static str;
63
64    fn created_at_column() -> &'static str;
65
66    /// Bind an admin user-list field to its actual model column. Derived models
67    /// provide all declared fields, including renamed physical columns. Manual
68    /// models may add their plugin/application field bindings explicitly.
69    #[must_use]
70    fn list_users_column(field: &str) -> Option<&'static str> {
71        match field {
72            "id" => Some(Self::id_column()),
73            "email" => Some(Self::email_column()),
74            "name" => Some(Self::name_column()),
75            "username" => Self::username_column(),
76            "createdAt" => Some(Self::created_at_column()),
77            _ => None,
78        }
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(alibi_core::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: &alibi_core::field_policy::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    ///
133    /// # Errors
134    ///
135    /// Returns an error if the field value is unsupported by the model column.
136    fn set_additional_field(
137        _active: &mut ActiveRow,
138        _column: &'static str,
139        _value: SqlValue,
140        _backend: Engine,
141    ) -> AuthResult<()> {
142        Err(alibi_core::AuthError::internal(
143            "the session schema cannot stage additional fields",
144        ))
145    }
146
147    fn id_column() -> &'static str;
148
149    fn token_column() -> &'static str;
150
151    fn user_id_column() -> &'static str;
152
153    fn active_column() -> &'static str;
154
155    fn expires_at_column() -> &'static str;
156
157    fn created_at_column() -> &'static str;
158
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the identifier is invalid for this model's ID type.
163    fn parse_id(id: &str) -> AuthResult<SqlValue>;
164
165    ///
166    /// # Errors
167    ///
168    /// Returns an error if the user identifier is invalid for this model's ID type.
169    fn parse_user_id(user_id: &str) -> AuthResult<SqlValue>;
170
171    fn new_active(
172        id: Option<SqlValue>,
173        token: String,
174        create_session: CreateSession,
175        now: DateTime<Utc>,
176    ) -> ActiveRow;
177
178    fn set_expires_at(active: &mut ActiveRow, expires_at: DateTime<Utc>);
179
180    fn set_updated_at(active: &mut ActiveRow, updated_at: DateTime<Utc>);
181
182    fn set_active_organization_id(active: &mut ActiveRow, organization_id: Option<String>);
183
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if the configured model does not support an active-team field.
188    fn set_active_team_id(active: &mut ActiveRow, team_id: Option<String>) -> AuthResult<()> {
189        drop((active, team_id));
190        Err(alibi_core::AuthError::internal(
191            "the session schema has no active-team field",
192        ))
193    }
194}
195
196pub trait SqlxAccountModel: AuthAccount + SqlxModel {
197    /// Bind configured fields to actual model columns while retaining raw numbers.
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if configured additional fields cannot be bound to model columns.
202    fn additional_field_bindings(
203        fields: &alibi_core::field_policy::FieldValues,
204        _backend: Engine,
205    ) -> AuthResult<Vec<(&'static str, SqlValue)>> {
206        if fields.is_empty() {
207            return Ok(Vec::new());
208        }
209        Err(alibi_core::AuthError::internal(
210            "the account schema has no additional field bindings",
211        ))
212    }
213
214    ///
215    /// # Errors
216    ///
217    /// Returns an error if the field value is unsupported by the model column.
218    fn set_additional_field(
219        _active: &mut ActiveRow,
220        _column: &'static str,
221        _value: SqlValue,
222        _backend: Engine,
223    ) -> AuthResult<()> {
224        Err(alibi_core::AuthError::internal(
225            "the account schema cannot stage additional fields",
226        ))
227    }
228
229    /// Physical access, refresh and ID token columns for operator conversion.
230    /// Handwritten models opt in explicitly; absence fails closed.
231    fn oauth_token_columns() -> Option<[&'static str; 3]> {
232        None
233    }
234
235    fn id_column() -> &'static str;
236    fn provider_id_column() -> &'static str;
237    fn account_id_column() -> &'static str;
238    fn user_id_column() -> &'static str;
239    fn created_at_column() -> &'static str;
240    ///
241    /// # Errors
242    ///
243    /// Returns an error if the identifier is invalid for this model's ID type.
244    fn parse_id(id: &str) -> AuthResult<SqlValue>;
245
246    ///
247    /// # Errors
248    ///
249    /// Returns an error if the user identifier is invalid for this model's ID type.
250    fn parse_user_id(user_id: &str) -> AuthResult<SqlValue>;
251
252    fn new_active(
253        id: Option<SqlValue>,
254        create_account: CreateAccount,
255        now: DateTime<Utc>,
256    ) -> ActiveRow;
257    fn apply_update(active: &mut ActiveRow, update: UpdateAccount, now: DateTime<Utc>);
258}
259
260pub trait SqlxVerificationModel: AuthVerification + SqlxModel {
261    fn id_column() -> &'static str;
262    fn identifier_column() -> &'static str;
263    fn value_column() -> &'static str;
264    fn expires_at_column() -> &'static str;
265    fn created_at_column() -> &'static str;
266    #[must_use]
267    fn updated_at_column() -> Option<&'static str> {
268        None
269    }
270    ///
271    /// # Errors
272    ///
273    /// Returns an error if the identifier is invalid for this model's ID type.
274    fn parse_id(id: &str) -> AuthResult<SqlValue>;
275
276    /// Convert a deterministic reservation key to this schema's ID type.
277    /// String schemas use the upstream key. UUID schemas can accept the UUID
278    /// derived from the same digest; numeric schemas must explicitly provide
279    /// a collision-resistant reservation strategy instead of truncating it.
280    ///
281    /// # Errors
282    ///
283    /// Returns an error if the reservation identifier is invalid for this model's ID type.
284    fn parse_reservation_id(encoded: &str, digest: [u8; 32]) -> AuthResult<SqlValue> {
285        if let Ok(id) = Self::parse_id(encoded) {
286            return Ok(id);
287        }
288        let mut bytes = [0; 16];
289        for (byte, source) in bytes.iter_mut().zip(digest) {
290            *byte = source;
291        }
292        Self::parse_id(&uuid::Uuid::from_bytes(bytes).to_string()).map_err(|_error| {
293            alibi_core::AuthError::internal(
294                "the verification schema cannot represent deterministic reservation IDs",
295            )
296        })
297    }
298
299    fn new_active(
300        id: Option<SqlValue>,
301        verification: CreateVerification,
302        now: DateTime<Utc>,
303    ) -> ActiveRow;
304}