Skip to main content

valence_core/runtime/
mod.rs

1//! Valence runtime handle and builder.
2
3mod builder;
4mod factory;
5
6pub use builder::ValenceBuilder;
7pub use factory::{RouterValenceFactory, RouterValenceFactoryConfig, ValenceFactory};
8
9use crate::actor::Actor;
10use crate::backend::DatabaseBackend;
11use crate::error::Result;
12use crate::owner_ref::OwnerRef;
13use crate::ports::actor::ActorFactory;
14use crate::ports::endpoints::DatabaseEndpointResolver;
15use crate::ports::secrets::SecretProvider;
16use crate::record_id::RecordId;
17use crate::request_cache::RequestPermissionCache;
18use crate::router::DatabaseRouter;
19use crate::router_key::router_key;
20use crate::schema::SchemaRegistry;
21use std::sync::Arc;
22use valence_telemetry::TelemetrySink;
23
24/// Host-assembled Valence handle.
25#[derive(Clone)]
26pub struct Valence {
27    pub(crate) router: Arc<DatabaseRouter>,
28    pub(crate) active_backend_key: String,
29    telemetry_sink: Arc<dyn TelemetrySink>,
30    secret_provider: Arc<dyn SecretProvider>,
31    actor_factory: Arc<dyn ActorFactory>,
32    endpoint_resolver: Arc<dyn DatabaseEndpointResolver>,
33    actor: Actor,
34    owner_override: Option<OwnerRef>,
35    permission_cache: Option<RequestPermissionCache>,
36}
37
38impl Valence {
39    pub fn builder() -> ValenceBuilder {
40        ValenceBuilder::new()
41    }
42
43    pub fn database_router(&self) -> &Arc<DatabaseRouter> {
44        &self.router
45    }
46
47    /// # Errors
48    ///
49    /// Returns an error when the requested operation cannot be completed.
50    pub fn active_backend(&self) -> Result<Arc<dyn DatabaseBackend>> {
51        self.router.resolve(&self.active_backend_key)
52    }
53
54    /// # Errors
55    ///
56    /// Returns an error when the requested operation cannot be completed.
57    pub fn backend_for_table(&self, table: &str) -> Result<Arc<dyn DatabaseBackend>> {
58        let Some(meta) = SchemaRegistry::global().get_schema(table) else {
59            return self.active_backend();
60        };
61        let eval = meta.schema.database_evaluator;
62        let key = router_key(eval.logical_name(), eval.engine_id());
63        self.router
64            .resolve(&key)
65            .or_else(|_| self.active_backend())
66            .inspect_err(|_e| {
67                crate::instrumentation::metrics::record_router_resolve_error(table);
68            })
69    }
70
71    pub fn telemetry_sink(&self) -> &Arc<dyn TelemetrySink> {
72        &self.telemetry_sink
73    }
74
75    pub fn secret_provider(&self) -> &Arc<dyn SecretProvider> {
76        &self.secret_provider
77    }
78
79    pub fn actor_factory(&self) -> &Arc<dyn ActorFactory> {
80        &self.actor_factory
81    }
82
83    pub fn endpoint_resolver(&self) -> &Arc<dyn DatabaseEndpointResolver> {
84        &self.endpoint_resolver
85    }
86
87    pub fn actor(&self) -> &Actor {
88        &self.actor
89    }
90
91    /// Clone this handle with a different actor.
92    ///
93    /// # Security
94    ///
95    /// Privileged: hosts must not expose raw `with_actor(Actor::System)` (or other elevated
96    /// actors) from untrusted client input. Bind actors from session/JWT at the host edge.
97    #[must_use]
98    pub fn with_actor(&self, actor: Actor) -> Self {
99        Self {
100            actor,
101            ..self.clone()
102        }
103    }
104
105    /// Clone this handle with an ownership override.
106    ///
107    /// # Security
108    ///
109    /// Privileged: do not accept client-supplied owner overrides without host authorization.
110    #[must_use]
111    pub fn with_owner_override(&self, owner: OwnerRef) -> Self {
112        Self {
113            owner_override: Some(owner),
114            ..self.clone()
115        }
116    }
117
118    pub fn owner_override(&self) -> Option<&OwnerRef> {
119        self.owner_override.as_ref()
120    }
121
122    /// Request-scoped permission check cache, when enabled on the builder.
123    pub fn permission_cache(&self) -> Option<&RequestPermissionCache> {
124        self.permission_cache.as_ref()
125    }
126
127    /// Navigate ManyToMany edges and return target [`RecordId`] values.
128    ///
129    /// # Security
130    ///
131    /// Raw edge read — no schema privacy. Do not expose as a client API without host authz.
132    ///
133    /// # Errors
134    ///
135    /// Returns an error when the requested operation cannot be completed.
136    pub async fn get_many_to_many_target_record_ids(
137        &self,
138        from: &RecordId,
139        edge_table: &str,
140    ) -> Result<Vec<RecordId>> {
141        let backend = self.active_backend()?;
142        backend.get_edge_targets(from, edge_table).await
143    }
144
145    /// Create a graph edge between two records in `edge_table`.
146    ///
147    /// # Security
148    ///
149    /// Raw edge write — no schema privacy. Hosts must authorize before calling.
150    ///
151    /// # Errors
152    ///
153    /// Returns an error when the requested operation cannot be completed.
154    pub async fn relate_edge(
155        &self,
156        edge_table: &str,
157        from: &RecordId,
158        to: &RecordId,
159    ) -> Result<()> {
160        let backend = self.active_backend()?;
161        backend.relate_edge(from, edge_table, to).await
162    }
163
164    /// Delete a graph edge between two records in `edge_table`.
165    ///
166    /// # Security
167    ///
168    /// Raw edge delete — no schema privacy. Hosts must authorize before calling.
169    ///
170    /// # Errors
171    ///
172    /// Returns an error when the requested operation cannot be completed.
173    pub async fn unrelate_edge(
174        &self,
175        edge_table: &str,
176        from: &RecordId,
177        to: &RecordId,
178    ) -> Result<()> {
179        let backend = self.active_backend()?;
180        backend.unrelate_edge(from, edge_table, to).await
181    }
182
183    /// # Errors
184    ///
185    /// Returns an error when the requested operation cannot be completed.
186    pub async fn ensure_unique_field_index(&self, table: &str, field: &str) -> Result<()> {
187        let backend = self.backend_for_table(table)?;
188        backend.define_unique_index(table, field).await
189    }
190
191    /// Apply native TTL (or warn once) for every schema that declares `ttl:`.
192    ///
193    /// Primary host wire-up after backends are registered. Scrapes
194    /// [`crate::SchemaRegistry`] (inventory / `auto_discover`) — no hand list of tables.
195    ///
196    /// # Errors
197    ///
198    /// See [`crate::ttl::ensure_ttl_for_all`].
199    pub async fn ensure_ttl_for_all(&self) -> Result<()> {
200        crate::ttl::ensure_ttl_for_all(self).await
201    }
202
203    /// Apply native TTL for `table`, or warn once when the backend is Deferred/Unsupported.
204    ///
205    /// Prefer [`Self::ensure_ttl_for_all`] at boot; use this for incremental/single-table cases.
206    ///
207    /// # Errors
208    ///
209    /// See [`crate::ttl::ensure_ttl_for_table`].
210    pub async fn ensure_ttl_for_table(&self, table: &str) -> Result<()> {
211        crate::ttl::ensure_ttl_for_table(self, table).await
212    }
213
214    /// Create typed physical tables for every schema in [`crate::SchemaRegistry`].
215    ///
216    /// # Errors
217    ///
218    /// See [`crate::storage_layout::ensure_typed_tables_from_registry`].
219    pub async fn ensure_typed_tables_from_registry(&self) -> Result<()> {
220        crate::storage_layout::ensure_typed_tables_from_registry(self).await
221    }
222
223    /// Additive-sync typed tables for every schema in [`crate::SchemaRegistry`].
224    ///
225    /// # Errors
226    ///
227    /// See [`crate::storage_layout::sync_typed_tables_from_registry`].
228    pub async fn sync_typed_tables_from_registry(&self) -> Result<()> {
229        crate::storage_layout::sync_typed_tables_from_registry(self).await
230    }
231
232    pub fn is_system(&self) -> bool {
233        self.actor.is_system()
234    }
235}
236
237impl std::fmt::Debug for Valence {
238    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        f.debug_struct("Valence")
240            .field("active_backend_key", &self.active_backend_key)
241            .finish_non_exhaustive()
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::ports::actor::JsonActorFactory;
249
250    #[test]
251    fn builder_requires_backend() {
252        let err = Valence::builder()
253            .actor_factory(Arc::new(JsonActorFactory))
254            .build()
255            .unwrap_err();
256        assert!(err.to_string().contains("at least one backend"));
257    }
258
259    #[test]
260    fn builder_accepts_actor_with_backend() {
261        use crate::backend::DatabaseBackend;
262        use async_trait::async_trait;
263        use std::sync::atomic::{AtomicUsize, Ordering};
264
265        #[derive(Debug)]
266        struct MockBackend;
267
268        #[async_trait]
269        impl DatabaseBackend for MockBackend {
270            fn engine_id(&self) -> &'static str {
271                "mem"
272            }
273
274            fn capabilities(&self) -> crate::backend::BackendCapabilities {
275                crate::backend::BackendCapabilities::mem()
276            }
277
278            async fn execute_compiled_query(
279                &self,
280                _compiled: &crate::compiled_query::CompiledQuery,
281            ) -> crate::error::Result<Vec<serde_json::Value>> {
282                Ok(vec![])
283            }
284
285            async fn get_record(
286                &self,
287                _table: &str,
288                _id: &str,
289            ) -> crate::error::Result<Option<serde_json::Value>> {
290                static GETS: AtomicUsize = AtomicUsize::new(0);
291                GETS.fetch_add(1, Ordering::SeqCst);
292                Ok(None)
293            }
294
295            async fn create_record(
296                &self,
297                _table: &str,
298                _content: serde_json::Value,
299            ) -> crate::error::Result<serde_json::Value> {
300                Ok(serde_json::json!({}))
301            }
302
303            async fn update_record(
304                &self,
305                _table: &str,
306                _id: &str,
307                _content: serde_json::Value,
308            ) -> crate::error::Result<serde_json::Value> {
309                Ok(serde_json::json!({}))
310            }
311
312            async fn upsert_record(
313                &self,
314                _table: &str,
315                _id: &str,
316                _content: serde_json::Value,
317            ) -> crate::error::Result<serde_json::Value> {
318                Ok(serde_json::json!({}))
319            }
320
321            async fn delete_record(&self, _table: &str, _id: &str) -> crate::error::Result<()> {
322                Ok(())
323            }
324
325            async fn relate_edge(
326                &self,
327                _from: &crate::record_id::RecordId,
328                _edge_table: &str,
329                _to: &crate::record_id::RecordId,
330            ) -> crate::error::Result<()> {
331                Ok(())
332            }
333
334            async fn unrelate_edge(
335                &self,
336                _from: &crate::record_id::RecordId,
337                _edge_table: &str,
338                _to: &crate::record_id::RecordId,
339            ) -> crate::error::Result<()> {
340                Ok(())
341            }
342
343            async fn get_edge_targets(
344                &self,
345                _from: &crate::record_id::RecordId,
346                _edge_table: &str,
347            ) -> crate::error::Result<Vec<crate::record_id::RecordId>> {
348                Ok(vec![])
349            }
350        }
351
352        let v = Valence::builder()
353            .add_backend("default", Arc::new(MockBackend))
354            .with_actor(Actor::System {
355                operation: "test".into(),
356            })
357            .build()
358            .expect("build");
359        assert!(v.is_system());
360        let _ = v.active_backend().expect("backend");
361    }
362}