Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/user/session/
builder.rs

1//! Builder for creating a fully-initialized Database in a single genesis entry.
2//!
3//! See [`User::new_database`] for the entry point. The builder collects
4//! settings, key policy, and a list of store initializers, then folds them all
5//! into one signed entry at [`DatabaseBuilder::build`] time so the new
6//! database is atomically constructed with its full initial shape.
7
8use std::collections::HashSet;
9use std::future::Future;
10use std::pin::Pin;
11
12use super::User;
13use crate::{
14    Database, Result, Store, Transaction, auth::crypto::PublicKey, crdt::Doc,
15    user::errors::UserError,
16};
17
18/// Type-erased per-store initialization closure. The HRTB lets the closure
19/// borrow the genesis `Transaction` for the duration of the returned future.
20type StoreInit = Box<
21    dyn for<'a> FnOnce(&'a Transaction) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>
22        + Send,
23>;
24
25enum KeyPolicy {
26    AutoGenerate { label: Option<String> },
27    UseExisting(PublicKey),
28}
29
30/// Chainable builder for constructing a new Database with all of its initial
31/// shape — settings, signing key, registered stores, and seed data — folded
32/// into one genesis entry.
33///
34/// Obtained via [`User::new_database`]. Terminal method is
35/// [`DatabaseBuilder::build`].
36///
37/// # Auto-generated keys on failure
38///
39/// When no key policy is set (or only [`Self::key_label`] is used), the
40/// builder generates a fresh signing key via [`User::add_private_key`]
41/// **before** running store initializers. If a store initializer or the
42/// genesis commit then fails, the generated key remains in the user's
43/// key store. `UserKeyManager` does not currently expose a key-removal
44/// API, so this leak is by design — it matches the semantics of the
45/// pre-existing `add_private_key` + `create_database` flow.
46///
47/// If avoiding the leak matters, call [`User::add_private_key`] yourself
48/// and pass the result via [`Self::with_key`]; the key persists either way
49/// but you keep ownership of when it was created.
50///
51/// # Example
52///
53/// ```ignore
54/// use eidetica::store::DocStoreInit;
55///
56/// let (db, key) = user.new_database()
57///     .name("agent:demo")
58///     .key_label("agent:demo")
59///     .empty_doc("config")
60///     .initialize_doc("meta", meta)
61///     .build()
62///     .await?;
63/// ```
64pub struct DatabaseBuilder<'u> {
65    user: &'u mut User,
66    settings: Doc,
67    key_policy: KeyPolicy,
68    store_inits: Vec<(String, StoreInit)>,
69}
70
71impl<'u> DatabaseBuilder<'u> {
72    pub(super) fn new(user: &'u mut User) -> Self {
73        Self {
74            user,
75            settings: Doc::new(),
76            key_policy: KeyPolicy::AutoGenerate { label: None },
77            store_inits: Vec::new(),
78        }
79    }
80
81    /// Set the database's display name (the `name` field in `_settings`).
82    /// Shortcut for the common case; for full control over the settings Doc
83    /// use [`Self::settings`] instead.
84    pub fn name(mut self, name: impl Into<String>) -> Self {
85        self.settings.set("name", name.into());
86        self
87    }
88
89    /// Replace the entire settings Doc. Overrides any prior [`Self::name`] call.
90    pub fn settings(mut self, settings: Doc) -> Self {
91        self.settings = settings;
92        self
93    }
94
95    /// Generate a fresh signing key with the given display label when
96    /// [`Self::create`] runs. Default behavior (no label) is also available by
97    /// not calling either key method.
98    pub fn key_label(mut self, label: impl Into<String>) -> Self {
99        self.key_policy = KeyPolicy::AutoGenerate {
100            label: Some(label.into()),
101        };
102        self
103    }
104
105    /// Use an existing key from the user's key manager rather than generating
106    /// a fresh one. `key` must already have been added to the user via
107    /// [`User::add_private_key`]; otherwise [`Self::create`] will fail when
108    /// resolving the signing key.
109    pub fn with_key(mut self, key: PublicKey) -> Self {
110        self.key_policy = KeyPolicy::UseExisting(key);
111        self
112    }
113
114    /// Register a store named `name` and run `init` against it inside the
115    /// genesis transaction. The closure body uses the Store's normal write
116    /// API. Pass a no-op closure to register an empty store.
117    ///
118    /// This is the generic primitive. Each Store module ships its own
119    /// extension trait providing ergonomic non-generic variants (for example
120    /// [`DocStoreInit::initialize_doc`](crate::store::DocStoreInit::initialize_doc)).
121    pub fn initialize_store<S, F, Fut>(mut self, name: impl Into<String>, init: F) -> Self
122    where
123        S: Store + Send + 'static,
124        F: FnOnce(S) -> Fut + Send + 'static,
125        Fut: Future<Output = Result<()>> + Send + 'static,
126    {
127        let name = name.into();
128        let name_for_open = name.clone();
129        let init_box: StoreInit = Box::new(move |txn| {
130            Box::pin(async move {
131                let store = S::open(txn, name_for_open).await?;
132                init(store).await
133            })
134        });
135        self.store_inits.push((name, init_box));
136        self
137    }
138
139    /// Resolve the key, run every store initializer inside a single genesis
140    /// transaction, commit, then perform the user-side tracking write.
141    /// Returns the new database and the public key it was created with.
142    pub async fn build(self) -> Result<(Database, PublicKey)> {
143        let DatabaseBuilder {
144            user,
145            settings,
146            key_policy,
147            store_inits,
148        } = self;
149
150        // Reject duplicate store names before doing any work.
151        let mut seen: HashSet<&str> = HashSet::new();
152        for (name, _) in &store_inits {
153            if !seen.insert(name.as_str()) {
154                return Err(UserError::DuplicateBuilderStore { name: name.clone() }.into());
155            }
156        }
157
158        // Resolve the signing key.
159        let key_id = match key_policy {
160            KeyPolicy::AutoGenerate { label } => user.add_private_key(label.as_deref()).await?,
161            KeyPolicy::UseExisting(k) => k,
162        };
163
164        // Fold all per-store initializers into one async callback for the
165        // genesis transaction. Each `init` is invoked once and consumed.
166        let database = user
167            .create_database_with_init(settings, &key_id, async move |txn| {
168                for (_, init) in store_inits {
169                    init(txn).await?;
170                }
171                Ok(())
172            })
173            .await?;
174
175        Ok((database, key_id))
176    }
177}