boatramp_node/
handlers.rs1#[cfg(feature = "handlers")]
10use crate::error::Error;
11use crate::error::Result;
12use boatramp_core::kv::KvStore;
13use std::path::Path;
14use std::sync::Arc;
15
16#[cfg(feature = "handlers")]
20pub fn build_handler_runtime(
21 kv: Arc<dyn KvStore>,
22 storage: Arc<dyn boatramp_core::Storage>,
23 data_dir: &Path,
24 handlers_cfg: Option<&crate::config::HandlersConfig>,
25 messaging_override: Option<Arc<dyn boatramp_core::messaging::Messaging>>,
26 max_blob_bytes: u64,
27 max_component_bytes: u64,
28) -> Result<boatramp_server::HandlerRuntime> {
29 let limits = boatramp_handlers::Limits::default();
32 let engine = if handlers_cfg.is_some_and(|h| h.pooling) {
33 boatramp_handlers::HandlerEngine::with_pooling(limits, 64)?
34 } else {
35 boatramp_handlers::HandlerEngine::new(limits, 64)?
36 };
37 let sql = build_sql_backends(handlers_cfg.and_then(|h| h.bindings.sql.as_ref()), data_dir)?;
38 let messaging: Arc<dyn boatramp_core::messaging::Messaging> = messaging_override
41 .unwrap_or_else(|| {
42 Arc::new(boatramp_core::messaging::LogMessaging::new(
43 storage.clone(),
44 kv.clone(),
45 ))
46 });
47 let runtime =
48 boatramp_server::HandlerRuntime::new(engine, kv, storage, Some(sql), Some(messaging));
49 runtime.set_max_blob_bytes(max_blob_bytes);
51 runtime.set_max_component_bytes(max_component_bytes);
52 Ok(runtime)
53}
54
55#[cfg(feature = "handlers")]
60fn build_sql_backends(
61 cfg: Option<&crate::config::SqlBindingConfig>,
62 data_dir: &Path,
63) -> Result<Arc<dyn boatramp_core::sql::SqlBackends>> {
64 let resolve_env = |var: &Option<String>| -> Result<Option<String>> {
65 match var {
66 Some(var) => Ok(Some(
67 std::env::var(var).map_err(|_| Error::SqlEnvUnset(var.clone()))?,
68 )),
69 None => Ok(None),
70 }
71 };
72
73 let backend = match cfg.and_then(|c| c.url.as_ref()) {
74 Some(url) => {
77 let cfg = cfg.expect("url implies cfg");
78 let admin_url = cfg.admin_url.as_ref().ok_or(Error::SqlAdminUrlRequired)?;
79 let token = resolve_env(&cfg.token_env)?.unwrap_or_default();
80 let admin_token = resolve_env(&cfg.admin_token_env)?;
81 let backends = boatramp_storage::LibsqlSqlBackends::remote(
82 url.clone(),
83 admin_url.clone(),
84 token,
85 admin_token,
86 );
87 match &cfg.replica_url {
89 Some(replica_url) => backends.with_read_replica(replica_url.clone()),
90 None => backends,
91 }
92 }
93 None => {
95 let dir = cfg
96 .and_then(|c| c.dir.clone())
97 .unwrap_or_else(|| data_dir.join("handlers-sql"));
98 boatramp_storage::LibsqlSqlBackends::local(dir)
99 }
100 };
101 let preview_mode = match cfg.and_then(|c| c.preview_mode.as_deref()) {
103 None | Some("empty") => boatramp_core::sql::PreviewSqlMode::Empty,
104 Some("branch") => boatramp_core::sql::PreviewSqlMode::Branch,
105 Some("shared") => boatramp_core::sql::PreviewSqlMode::Shared,
106 Some(other) => return Err(Error::UnknownPreviewMode(other.to_string())),
107 };
108 let preview_init = match cfg.and_then(|c| c.preview_init.as_ref()) {
109 Some(path) => {
110 Some(
111 std::fs::read_to_string(path).map_err(|err| Error::PreviewInitRead {
112 path: path.clone(),
113 source: err,
114 })?,
115 )
116 }
117 None => None,
118 };
119 let default: Arc<dyn boatramp_core::sql::SqlBackends> =
120 Arc::new(backend.with_preview_policy(preview_mode, preview_init));
121
122 let databases = cfg.map(|c| &c.databases);
126 if databases.is_none_or(std::collections::BTreeMap::is_empty) {
127 return Ok(default);
128 }
129 let databases = databases.expect("checked non-empty above");
130
131 #[cfg(any(feature = "sql-postgres", feature = "sql-mysql"))]
132 {
133 use boatramp_storage::sql_sqlx::{
134 connect, CompositeSqlBackends, ExternalSqlKind, ExternalSqlOptions,
135 };
136 let mut composite = CompositeSqlBackends::new(default);
137 for (name, db) in databases {
138 let kind = ExternalSqlKind::parse(&db.kind).ok_or_else(|| Error::SqlExternalKind {
139 name: name.clone(),
140 kind: db.kind.clone(),
141 })?;
142 if db.url_env.trim().is_empty() {
143 return Err(Error::SqlExternalUrlEnvMissing(name.clone()));
144 }
145 let url =
147 std::env::var(&db.url_env).map_err(|_| Error::SqlEnvUnset(db.url_env.clone()))?;
148 let read_url = match &db.read_url_env {
149 Some(var) => Some(std::env::var(var).map_err(|_| Error::SqlEnvUnset(var.clone()))?),
150 None => None,
151 };
152 let opts = ExternalSqlOptions::new(url)
153 .with_read_url(read_url)
154 .with_max_connections(db.pool_max)
155 .read_only(db.read_only)
156 .with_connect_timeout(db.connect_timeout_secs.map(std::time::Duration::from_secs));
157 let external = connect(kind, &opts).map_err(|source| Error::SqlExternalConnect {
158 name: name.clone(),
159 source,
160 })?;
161 composite = composite.with_external(name.clone(), external, db.allow_preview);
162 }
163 Ok(Arc::new(composite))
164 }
165 #[cfg(not(any(feature = "sql-postgres", feature = "sql-mysql")))]
166 {
167 let name = databases.keys().next().cloned().unwrap_or_default();
168 Err(Error::SqlExternalUnavailable(name))
169 }
170}
171
172#[cfg(not(feature = "handlers"))]
173pub fn build_handler_runtime(
174 _kv: Arc<dyn KvStore>,
175 _storage: Arc<dyn boatramp_core::Storage>,
176 _data_dir: &Path,
177 _handlers_cfg: Option<&crate::config::HandlersConfig>,
178 _messaging_override: Option<Arc<dyn boatramp_core::messaging::Messaging>>,
179 _max_blob_bytes: u64,
180 _max_component_bytes: u64,
181) -> Result<boatramp_server::HandlerRuntime> {
182 Ok(boatramp_server::HandlerRuntime::disabled())
183}