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}