Skip to main content

alibi_core/config/
id.rs

1//! Policies for identifiers assigned by database adapters.
2use crate::{AuthError, AuthResult};
3use std::sync::Arc;
4
5/// A callback returning `None` rejects creation rather than generating a fallback.
6pub trait DatabaseIdGenerator: Send + Sync {
7    fn generate(&self, model: &str, size: Option<usize>) -> AuthResult<Option<String>>;
8}
9impl<F> DatabaseIdGenerator for F
10where
11    F: Fn(&str, Option<usize>) -> AuthResult<Option<String>> + Send + Sync,
12{
13    fn generate(&self, model: &str, size: Option<usize>) -> AuthResult<Option<String>> {
14        self(model, size)
15    }
16}
17
18#[derive(Clone)]
19pub enum DatabaseIdStrategy {
20    Uuid,
21    Serial,
22    Custom(Arc<dyn DatabaseIdGenerator>),
23}
24impl std::fmt::Debug for DatabaseIdStrategy {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.write_str(match self {
27            Self::Uuid => "Uuid",
28            Self::Serial => "Serial",
29            Self::Custom(_) => "Custom(..)",
30        })
31    }
32}
33impl super::AdvancedDatabaseConfig {
34    #[must_use]
35    pub fn serial_ids(&self) -> bool {
36        matches!(self.generate_id, Some(DatabaseIdStrategy::Serial))
37            || (self.generate_id.is_none() && self.use_number_id)
38    }
39    /// `None` leaves generation to the model or the database's serial allocator.
40    pub fn generated_id(&self, model: &str) -> AuthResult<Option<String>> {
41        match &self.generate_id {
42            Some(DatabaseIdStrategy::Uuid) => Ok(Some(uuid::Uuid::new_v4().to_string())),
43            Some(DatabaseIdStrategy::Custom(generator)) => generator
44                .generate(model, None)
45                .map_err(crate::store::adapter::callback_error)?
46                .map(Some)
47                .ok_or_else(|| {
48                    AuthError::CallbackFailure(Box::new(AuthError::internal(
49                        "Application ID generator returned no identifier",
50                    )))
51                }),
52            _ => Ok(None),
53        }
54    }
55}
56
57/// Statements for a persistent atomic per-model sequence, also usable with text IDs.
58/// Allocation must run on the same connection/transaction as the record creation.
59#[doc(hidden)]
60pub fn serial_id_statements(table: &str, column: &str, postgres: bool) -> [String; 2] {
61    let quote = |value: &str| format!("\"{}\"", value.replace('"', "\"\""));
62    let table_sql = quote(table);
63    let column_sql = quote(column);
64    let key = format!("'{table}'", table = table.replace('\'', "''"));
65    let numeric = if postgres {
66        format!(" WHERE CAST({column_sql} AS TEXT) ~ '^[0-9]+$'")
67    } else {
68        " WHERE 1 = 1".into()
69    };
70    let next = if postgres {
71        "GREATEST(alibi_id_sequences.value + 1, EXCLUDED.value)"
72    } else {
73        "MAX(alibi_id_sequences.value + 1, EXCLUDED.value)"
74    };
75    [
76        "CREATE TABLE IF NOT EXISTS alibi_id_sequences (model TEXT PRIMARY KEY, value BIGINT NOT NULL)".into(),
77        format!("INSERT INTO alibi_id_sequences (model, value) SELECT {key}, COALESCE(MAX(CAST(CAST({column_sql} AS TEXT) AS BIGINT)), 0) + 1 FROM {table_sql}{numeric} ON CONFLICT (model) DO UPDATE SET value = {next} RETURNING value"),
78    ]
79}