1use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use std::sync::atomic::AtomicU64;
17
18use axum::{
19 extract::{Path as AxPath, State, Query as AxQuery},
20 http::{HeaderMap, StatusCode},
21 response::{IntoResponse, Response, sse::{Event, KeepAlive, Sse}},
22 routing::{delete, get, post},
23 Json, Router,
24};
25use dashmap::DashMap;
26use serde::Deserialize;
27use serde_json::{json, Value};
28use tokio::sync::{broadcast, RwLock};
29use tokio_stream::wrappers::BroadcastStream;
30use tokio_stream::StreamExt as _;
31
32use crate::db::Db;
33use crate::nql;
34use crate::store::Node;
35
36const LOG_CHANNEL_CAP: usize = 512;
39const SUB_CHANNEL_CAP: usize = 256;
40
41type SubKey = (String, u64); type SubVal = (String, String, broadcast::Sender<String>); macro_rules! nlog {
51 ($tx:expr, $($arg:tt)*) => {{
52 let line = format!($($arg)*);
53 println!("{}", line);
54 let _ = $tx.send(line);
55 }};
56}
57
58#[derive(Clone)]
61pub struct Manager {
62 inner: Arc<RwLock<ManagerInner>>,
63 pub token: Option<String>,
64 pub log_tx: broadcast::Sender<String>,
66 subs: Arc<DashMap<SubKey, SubVal>>,
68 sub_ctr: Arc<AtomicU64>,
69 #[cfg(feature = "cast")]
73 pub caster: Option<crate::cast::Caster>,
74}
75
76struct ManagerInner {
77 data_dir: PathBuf,
78 dbs: HashMap<String, Arc<Db>>,
79 tmk: Option<[u8; 32]>,
80 memory_mode: bool,
81}
82
83impl Manager {
84 pub fn new(data_dir: &Path, tmk: Option<[u8; 32]>, token: Option<String>, memory_mode: bool) -> Self {
85 let (log_tx, _) = broadcast::channel(LOG_CHANNEL_CAP);
86 Self {
87 inner: Arc::new(RwLock::new(ManagerInner {
88 data_dir: data_dir.to_path_buf(),
89 dbs: HashMap::new(),
90 tmk,
91 memory_mode,
92 })),
93 token,
94 log_tx,
95 #[cfg(feature = "cast")]
96 caster: None,
97 subs: Arc::new(DashMap::new()),
98 sub_ctr: Arc::new(AtomicU64::new(1)),
99 }
100 }
101
102 fn subscribe(&self, db: &str, nql: String) -> (u64, broadcast::Receiver<String>) {
104 use std::sync::atomic::Ordering;
105 let id = self.sub_ctr.fetch_add(1, Ordering::Relaxed);
106 let (tx, rx) = broadcast::channel(SUB_CHANNEL_CAP);
107 self.subs.insert((db.to_string(), id), (nql, String::new(), tx));
108 (id, rx)
109 }
110
111 fn unsubscribe(&self, db: &str, sub_id: u64) {
113 self.subs.remove(&(db.to_string(), sub_id));
114 }
115
116 fn notify_subscribers(&self, db: &str, db_arc: &Arc<crate::db::Db>) {
118 let keys: Vec<SubKey> = self.subs.iter()
119 .filter(|e| e.key().0 == db)
120 .map(|e| e.key().clone())
121 .collect();
122
123 for key in keys {
124 if let Some(mut entry) = self.subs.get_mut(&key) {
125 let (nql, last_hash, tx) = entry.value_mut();
126 let rows = match crate::nql::query(db_arc, nql) {
128 Ok((rows, _)) => rows,
129 Err(_) => continue,
130 };
131 let new_hash = format!("{:?}", rows.iter().map(|r| r.to_string()).collect::<Vec<_>>());
133 if new_hash == *last_hash { continue; }
134 *last_hash = new_hash;
135 let event = json!({
137 "sub_id": key.1,
138 "db": &key.0,
139 "nql": nql.as_str(),
140 "rows": rows,
141 "count": rows.len(),
142 });
143 let _ = tx.send(event.to_string());
144 }
145 }
146 }
147
148 pub async fn open_all(&self) -> anyhow::Result<()> {
150 let (data_dir, tmk, memory_mode) = {
151 let inner = self.inner.read().await;
152 (inner.data_dir.clone(), inner.tmk, inner.memory_mode)
153 };
154 if memory_mode { return Ok(()); }
156 if !data_dir.exists() {
157 std::fs::create_dir_all(&data_dir)?;
158 return Ok(());
159 }
160 let mut names = vec![];
161 for entry in std::fs::read_dir(&data_dir)? {
162 let entry = entry?;
163 if entry.file_type()?.is_dir() {
164 names.push(entry.file_name().to_string_lossy().to_string());
165 }
166 }
167 let log_tx = self.log_tx.clone();
168 let mut inner = self.inner.write().await;
169 for name in names {
170 let db_path = inner.data_dir.join(&name);
171 let dek = tmk.map(|k| crate::store::Dek::from_tmk(&k, name.as_bytes()));
172 match Db::open(&db_path, dek) {
173 Ok(db) => {
174 nlog!(log_tx, " [nedbd] opened database {:?}", name);
175 let db_arc = Arc::new(db);
176 Db::start_cold_scan(Arc::clone(&db_arc));
177 Db::start_manifest_ticker(Arc::clone(&db_arc), 1000);
179 inner.dbs.insert(name, db_arc);
180 }
181 Err(e) => nlog!(log_tx, " [nedbd] ERROR opening {:?}: {}", name, e),
182 }
183 }
184 Ok(())
185 }
186
187 async fn get_db(&self, name: &str) -> Option<Arc<Db>> {
188 self.inner.read().await.dbs.get(name).cloned()
189 }
190
191 async fn create_db(&self, name: &str) -> anyhow::Result<Arc<Db>> {
192 let (data_dir, tmk, memory_mode) = {
193 let inner = self.inner.read().await;
194 (inner.data_dir.clone(), inner.tmk, inner.memory_mode)
195 };
196 let db = if memory_mode {
197 Arc::new(Db::in_memory())
199 } else {
200 let db_path = data_dir.join(name);
201 let dek = tmk.map(|k| crate::store::Dek::from_tmk(&k, name.as_bytes()));
202 let db = Arc::new(Db::open(&db_path, dek)?);
203 Db::start_cold_scan(Arc::clone(&db));
204 Db::start_manifest_ticker(Arc::clone(&db), 1000);
205 db
206 };
207 self.inner.write().await.dbs.insert(name.to_string(), db.clone());
208 Ok(db)
209 }
210
211 async fn drop_db(&self, name: &str) -> bool {
212 let db = self.inner.write().await.dbs.remove(name);
213 if let Some(db) = db {
214 db.flush_manifest_if_dirty();
216 let data_dir = self.inner.read().await.data_dir.clone();
217 let _ = std::fs::remove_dir_all(data_dir.join(name));
218 true
219 } else {
220 false
221 }
222 }
223
224 pub async fn flush_all(&self) {
226 let inner = self.inner.read().await;
227 for db in inner.dbs.values() {
228 db.flush_all(); }
230 }
231
232 async fn names(&self) -> Vec<String> {
233 self.inner.read().await.dbs.keys().cloned().collect()
234 }
235
236 pub fn log(&self, msg: impl Into<String>) {
238 let line = msg.into();
239 println!("{}", line);
240 let _ = self.log_tx.send(line);
241 }
242
243 fn check_auth(&self, headers: &HeaderMap) -> bool {
244 match &self.token {
245 None => true,
246 Some(required) => {
247 if let Some(auth) = headers.get("authorization") {
248 if let Ok(s) = auth.to_str() {
249 return s == format!("Bearer {}", required);
250 }
251 }
252 false
253 }
254 }
255 }
256}
257
258fn err(status: StatusCode, msg: &str) -> Response {
261 (status, Json(json!({"error": msg}))).into_response()
262}
263
264fn ok(body: Value) -> Response {
265 (StatusCode::OK, Json(body)).into_response()
266}
267
268fn db_seq_head(db: &Db) -> (u64, String) {
272 let seq = db.seq.load(std::sync::atomic::Ordering::SeqCst);
273 let head = db.head();
274 (seq, head)
275}
276
277async fn health(State(mgr): State<Manager>) -> Response {
280 let names = mgr.names().await;
281 let inner = mgr.inner.read().await;
282 ok(json!({
283 "ok": true,
284 "service": "nedbd",
285 "version": env!("CARGO_PKG_VERSION"),
286 "engine": "dag",
287 "memory": inner.memory_mode,
288 "databases": names,
289 "encrypted": inner.tmk.is_some(),
290 }))
291}
292
293async fn list_databases(State(mgr): State<Manager>, headers: HeaderMap) -> Response {
294 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
295 let names = mgr.names().await;
296 let summaries: Vec<Value> = {
297 let inner = mgr.inner.read().await;
298 names.iter().map(|n| {
299 if let Some(db) = inner.dbs.get(n) {
300 let (seq, head) = db_seq_head(db);
301 json!({"name": n, "seq": seq, "head": head, "collections": db.id_index.collections()})
302 } else {
303 json!({"name": n})
304 }
305 }).collect()
306 };
307 ok(json!({"databases": summaries}))
308}
309
310#[derive(Deserialize)]
311struct CreateDbBody { name: String }
312
313async fn create_database(
314 State(mgr): State<Manager>,
315 headers: HeaderMap,
316 Json(body): Json<CreateDbBody>,
317) -> Response {
318 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
319 if body.name.is_empty() { return err(StatusCode::BAD_REQUEST, "name is required"); }
320 match mgr.create_db(&body.name).await {
321 Ok(db) => {
322 let (seq, head) = db_seq_head(&db);
323 (StatusCode::CREATED, Json(json!({"database": {"name": body.name, "seq": seq, "head": head}}))).into_response()
324 }
325 Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
326 }
327}
328
329async fn get_database(
330 State(mgr): State<Manager>,
331 headers: HeaderMap,
332 AxPath(name): AxPath<String>,
333) -> Response {
334 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
335 match mgr.get_db(&name).await {
336 None => err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
337 Some(db) => {
338 let (seq, head) = db_seq_head(&db);
339 ok(json!({"name": name, "seq": seq, "head": head, "collections": db.id_index.collections()}))
340 }
341 }
342}
343
344async fn drop_database(
345 State(mgr): State<Manager>,
346 headers: HeaderMap,
347 AxPath(name): AxPath<String>,
348) -> Response {
349 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
350 let dropped = mgr.drop_db(&name).await;
351 ok(json!({"dropped": dropped}))
352}
353
354#[derive(Deserialize)]
355struct QueryBody { nql: String }
356
357#[cfg_attr(not(feature = "cast"), allow(dead_code))]
365#[derive(Deserialize)]
366struct CastBody {
367 prompt: String,
368 #[serde(default)]
372 execute: bool,
373}
374
375#[cfg(feature = "cast")]
381async fn cast_prompt(
382 State(mgr): State<Manager>,
383 headers: HeaderMap,
384 AxPath(name): AxPath<String>,
385 Json(body): Json<CastBody>,
386) -> Response {
387 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
388
389 let caster = match &mgr.caster {
390 Some(c) => c,
391 None => return err(
392 StatusCode::SERVICE_UNAVAILABLE,
393 "cast is not enabled; start nedbd with --cast (or NEDBD_CAST=1) \
394 and place model.cast in the data directory",
395 ),
396 };
397
398 let db = match mgr.get_db(&name).await {
399 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
400 Some(db) => db,
401 };
402 if body.prompt.trim().is_empty() {
403 return err(StatusCode::BAD_REQUEST, "prompt is required");
404 }
405
406 let collections = db.id_index.collections();
409 let result = caster.cast_checked(&body.prompt, &collections);
410
411 let parse_err = match nql::parse(&result.nql) {
414 Ok(_) => None,
415 Err(e) => Some(e.to_string()),
416 };
417
418 let (seq, head) = db_seq_head(&db);
419 let mut out = json!({
420 "prompt": body.prompt,
421 "nql": result.nql,
422 "valid": parse_err.is_none(),
423 "collection": result.collection,
424 "collection_known": result.collection_known,
425 "collections": collections,
426 "executed": false,
427 "seq": seq,
428 "head": head,
429 });
430
431 if let Some(d) = &result.drift {
438 out["drift"] = json!(d);
439 }
440
441 if let Some(e) = parse_err {
442 out["error"] = json!(format!("NQL error: {}", e));
445 return (StatusCode::UNPROCESSABLE_ENTITY, Json(out)).into_response();
446 }
447
448 if !result.collection_known {
449 out["error"] = json!(format!(
453 "collection {:?} does not exist in {:?}",
454 result.collection.unwrap_or_default(), name
455 ));
456 return (StatusCode::UNPROCESSABLE_ENTITY, Json(out)).into_response();
457 }
458
459 if !body.execute {
460 return ok(out);
461 }
462
463 let nql_text = out["nql"].as_str().unwrap_or("").to_string();
465 match nql::query(&db, &nql_text) {
466 Ok((rows, count)) => {
467 out["executed"] = json!(true);
468 out["rows"] = json!(rows);
469 out["count"] = json!(count);
470 ok(out)
471 }
472 Err(e) => {
473 out["error"] = json!(format!("NQL error: {}", e));
474 (StatusCode::BAD_REQUEST, Json(out)).into_response()
475 }
476 }
477}
478
479#[cfg(not(feature = "cast"))]
482async fn cast_prompt(
483 State(_mgr): State<Manager>,
484 _headers: HeaderMap,
485 AxPath(_name): AxPath<String>,
486 Json(_body): Json<CastBody>,
487) -> Response {
488 err(
489 StatusCode::NOT_IMPLEMENTED,
490 "this nedbd was built without the `cast` feature; \
491 rebuild with --features cast to enable natural-language planning",
492 )
493}
494
495async fn query_database(
496 State(mgr): State<Manager>,
497 headers: HeaderMap,
498 AxPath(name): AxPath<String>,
499 Json(body): Json<QueryBody>,
500) -> Response {
501 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
502 let db = match mgr.get_db(&name).await {
503 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
504 Some(db) => db,
505 };
506 if body.nql.trim().is_empty() {
507 return err(StatusCode::BAD_REQUEST, "nql is required");
508 }
509 match nql::query(&db, &body.nql) {
510 Ok((rows, count)) => {
511 let (seq, head) = db_seq_head(&db);
512 ok(json!({"rows": rows, "count": count, "seq": seq, "head": head}))
513 }
514 Err(e) => err(StatusCode::BAD_REQUEST, &format!("NQL error: {}", e)),
515 }
516}
517
518#[derive(Deserialize)]
519struct PutBody {
520 coll: String,
521 id: String,
522 doc: Value,
523 caused_by: Option<Vec<serde_json::Value>>,
524 valid_from: Option<String>,
525 valid_to: Option<String>,
526 #[allow(dead_code)] evidence: Option<String>,
527 #[allow(dead_code)] confidence: Option<f64>,
528 #[allow(dead_code)] client: Option<String>,
529 #[allow(dead_code)] nonce: Option<u64>,
530 #[allow(dead_code)] idem: Option<String>,
531}
532
533#[derive(Deserialize)]
534struct LinkBody {
535 frm: String,
536 rel: String,
537 to: String,
538}
539
540async fn put_document(
541 State(mgr): State<Manager>,
542 headers: HeaderMap,
543 AxPath(name): AxPath<String>,
544 Json(body): Json<PutBody>,
545) -> Response {
546 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
547 let db = match mgr.get_db(&name).await {
548 None => {
549 match mgr.create_db(&name).await {
551 Ok(db) => db,
552 Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
553 }
554 }
555 Some(db) => db,
556 };
557 if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
560 return err(StatusCode::SERVICE_UNAVAILABLE,
561 "database startup in progress — reads available, writes retry in a moment");
562 }
563 let caused_by: Vec<String> = body.caused_by.unwrap_or_default()
565 .into_iter()
566 .filter_map(|v| match v {
567 serde_json::Value::String(s) => Some(s),
568 serde_json::Value::Number(n) => {
569 n.as_u64().and_then(|seq| db.get_hash_by_seq(seq))
570 }
571 _ => None,
572 })
573 .collect();
574 let coll = body.coll.clone();
577 let id = body.id.clone();
578 let doc = body.doc.clone();
579 let vf = body.valid_from.clone();
580 let vt = body.valid_to.clone();
581 let db2 = Arc::clone(&db);
582 let result = tokio::task::spawn_blocking(move || {
583 db2.put(&coll, &id, doc, caused_by, vf, vt)
584 }).await;
585 match result {
586 Err(join_err) => err(StatusCode::INTERNAL_SERVER_ERROR, &join_err.to_string()),
587 Ok(Err(e)) => err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
588 Ok(Ok(node)) => {
589 let (seq, head) = db_seq_head(&db);
590 mgr.notify_subscribers(&name, &db);
591 ok(json!({"ok": true, "doc": node_to_response(&node), "seq": seq, "head": head}))
592 }
593 }
594}
595
596fn node_to_response(node: &Node) -> Value {
597 json!({
598 "_id": node.id,
599 "_hash": node.hash,
600 "_seq": node.seq,
601 "_coll": node.coll,
602 "data": node.data,
603 })
604}
605
606async fn link_document(
607 State(mgr): State<Manager>,
608 headers: HeaderMap,
609 AxPath(name): AxPath<String>,
610 Json(body): Json<LinkBody>,
611) -> Response {
612 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
613 let db = match mgr.get_db(&name).await {
614 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
615 Some(db) => db,
616 };
617 if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
618 return err(StatusCode::SERVICE_UNAVAILABLE, "startup scan in progress");
619 }
620 match db.link(&body.frm, &body.rel, &body.to) {
621 Ok(()) => {
622 let (seq, head) = db_seq_head(&db);
623 ok(json!({"ok": true, "frm": body.frm, "rel": body.rel, "to": body.to, "seq": seq, "head": head}))
624 }
625 Err(e) => err(StatusCode::BAD_REQUEST, &e.to_string()),
626 }
627}
628
629async fn get_document(
653 State(mgr): State<Manager>,
654 headers: HeaderMap,
655 AxPath((name, coll, id)): AxPath<(String, String, String)>,
656 AxQuery(q): AxQuery<GetRowQuery>,
657) -> Response {
658 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
659 let db = match mgr.get_db(&name).await {
660 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
661 Some(db) => db,
662 };
663 let node = match q.as_of {
664 Some(seq) => db.get_as_of(&coll, &id, seq),
665 None => db.get(&coll, &id),
666 };
667 let (seq, head) = db_seq_head(&db);
678 let row = match node {
679 None => Value::Null,
680 Some(n) => crate::nql::node_to_json(&n),
681 };
682 ok(json!({"row": row, "seq": seq, "head": head}))
683}
684
685#[derive(Deserialize, Default)]
686struct GetRowQuery {
687 as_of: Option<u64>,
688}
689
690async fn delete_document(
691 State(mgr): State<Manager>,
692 headers: HeaderMap,
693 AxPath((name, coll, id)): AxPath<(String, String, String)>,
694) -> Response {
695 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
696 let db = match mgr.get_db(&name).await {
697 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
698 Some(db) => db,
699 };
700 let existed = match db.delete(&coll, &id) {
703 Ok(v) => v,
704 Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
705 };
706 let (seq, head) = db_seq_head(&db);
707 ok(json!({"ok": existed, "seq": seq, "head": head}))
708}
709
710#[derive(Deserialize)]
711struct BatchOp {
712 op: String,
713 coll: Option<String>,
714 id: Option<String>,
715 doc: Option<Value>,
716 caused_by: Option<Vec<serde_json::Value>>,
717}
718#[derive(Deserialize)]
719struct BatchBody { ops: Vec<BatchOp> }
720
721async fn batch_operations(
722 State(mgr): State<Manager>,
723 headers: HeaderMap,
724 AxPath(name): AxPath<String>,
725 Json(body): Json<BatchBody>,
726) -> Response {
727 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
728 let db = match mgr.get_db(&name).await {
729 None => match mgr.create_db(&name).await {
730 Ok(db) => db,
731 Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
732 },
733 Some(db) => db,
734 };
735
736 if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
737 return err(StatusCode::SERVICE_UNAVAILABLE,
738 "database startup in progress — reads available, writes retry in a moment");
739 }
740
741 let mut put_ops = vec![];
745 let mut del_ops: Vec<(String, String)> = vec![];
746 let mut op_order: Vec<(&str, usize)> = vec![]; for op in &body.ops {
749 let t = op.op.to_lowercase();
750 match t.as_str() {
751 "put" => {
752 let caused_by: Vec<String> = op.caused_by.clone().unwrap_or_default()
754 .into_iter()
755 .filter_map(|v| match v {
756 serde_json::Value::String(s) => Some(s),
757 serde_json::Value::Number(n) => {
758 n.as_u64().and_then(|seq| db.get_hash_by_seq(seq))
759 }
760 _ => None,
761 })
762 .collect();
763 op_order.push(("put", put_ops.len()));
764 put_ops.push((
765 op.coll.clone().unwrap_or_default(),
766 op.id.clone().unwrap_or_default(),
767 op.doc.clone().unwrap_or(json!({})),
768 caused_by,
769 None::<String>,
770 None::<String>,
771 ));
772 }
773 "del" | "delete" => {
774 op_order.push(("del", del_ops.len()));
775 del_ops.push((
776 op.coll.clone().unwrap_or_default(),
777 op.id.clone().unwrap_or_default(),
778 ));
779 }
780 _ => { op_order.push(("unknown", 0)); }
781 }
782 }
783
784 let put_results = if put_ops.is_empty() {
786 vec![]
787 } else {
788 match db.put_batch(put_ops) {
789 Ok(nodes) => nodes.into_iter().map(|n| json!({"op":"put","id":n.id,"seq":n.seq,"hash":n.hash})).collect(),
790 Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
791 }
792 };
793
794 let del_results: Vec<serde_json::Value> = del_ops.iter().map(|(coll, id)| {
796 match db.delete(coll, id) {
797 Ok(existed) => json!({"op":"del","id":id,"ok":existed}),
798 Err(e) => json!({"op":"del","id":id,"error":e.to_string()}),
799 }
800 }).collect();
801
802 let mut results = vec![];
804 for (kind, idx) in &op_order {
805 let r = match *kind {
806 "put" => put_results.get(*idx).cloned().unwrap_or(json!({"op":"put","error":"missing"})),
807 "del" => del_results.get(*idx).cloned().unwrap_or(json!({"op":"del","error":"missing"})),
808 _ => json!({"op": kind, "error": "unknown op"}),
809 };
810 results.push(r);
811 }
812 let (seq, head) = db_seq_head(&db);
813 mgr.notify_subscribers(&name, &db);
815 ok(json!({"results": results, "count": results.len(), "seq": seq, "head": head}))
816}
817
818#[derive(Deserialize)]
819struct IndexBody { coll: String, field: String, kind: Option<String> }
820
821async fn create_index(
822 State(mgr): State<Manager>,
823 headers: HeaderMap,
824 AxPath(name): AxPath<String>,
825 Json(body): Json<IndexBody>,
826) -> Response {
827 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
828 let db = match mgr.get_db(&name).await {
829 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
830 Some(db) => db,
831 };
832 let kind = body.kind.as_deref().unwrap_or("eq");
833 match kind {
834 "sorted" | "eq" => {
835 db.create_sorted_index(&body.coll, &body.field);
836 ok(json!({"ok": true, "coll": body.coll, "field": body.field, "kind": kind}))
837 }
838 _ => err(StatusCode::BAD_REQUEST, &format!("unknown index kind: {}", kind)),
839 }
840}
841
842async fn verify_database(
843 State(mgr): State<Manager>,
844 headers: HeaderMap,
845 AxPath(name): AxPath<String>,
846) -> Response {
847 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
848 let db = match mgr.get_db(&name).await {
849 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
850 Some(db) => db,
851 };
852 let (ok_count, tampered) = db.verify();
853 let (seq, head) = db_seq_head(&db);
854 ok(json!({
855 "ok": tampered.is_empty(),
856 "seq": seq,
857 "head": head,
858 "tamper_evident": true,
859 "objects_checked": ok_count,
860 "tampered": tampered,
861 }))
862}
863
864async fn checkpoint(
865 State(mgr): State<Manager>,
866 headers: HeaderMap,
867 AxPath(name): AxPath<String>,
868) -> Response {
869 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
870 let db = match mgr.get_db(&name).await {
871 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
872 Some(db) => db,
873 };
874 let (seq, head) = db_seq_head(&db);
875 ok(json!({"ok": true, "head": head, "seq": seq}))
877}
878
879#[derive(Deserialize)]
880struct LogQuery { limit: Option<usize> }
881
882async fn get_log(
883 State(mgr): State<Manager>,
884 headers: HeaderMap,
885 AxPath(name): AxPath<String>,
886 AxQuery(q): AxQuery<LogQuery>,
887) -> Response {
888 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
889 let db = match mgr.get_db(&name).await {
890 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
891 Some(db) => db,
892 };
893 let limit = q.limit.unwrap_or(50);
894 let mut log_entries: Vec<Value> = db.objects.all_hashes()
896 .filter_map(|h| db.objects.read(&h).ok())
897 .take(limit)
898 .map(|n| json!({
899 "seq": n.seq, "coll": n.coll, "id": n.id,
900 "hash": n.hash, "ts": n.ts, "op": "put"
901 }))
902 .collect();
903 log_entries.sort_by(|a, b|
904 b["seq"].as_u64().cmp(&a["seq"].as_u64())
905 );
906 log_entries.truncate(limit);
907 let (seq, head) = db_seq_head(&db);
908 ok(json!({"log": log_entries, "seq": seq, "head": head}))
909}
910
911async fn tip_database(
917 State(mgr): State<Manager>,
918 headers: HeaderMap,
919 AxPath(name): AxPath<String>,
920) -> Response {
921 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
922 let db = match mgr.get_db(&name).await {
923 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
924 Some(db) => db,
925 };
926 let (seq, head) = db_seq_head(&db);
927 let tip = db.tip().map(|n| serde_json::to_value(&n).unwrap_or(Value::Null));
928 ok(json!({"tip": tip, "seq": seq, "head": head}))
929}
930
931async fn tip_collection_database(
933 State(mgr): State<Manager>,
934 headers: HeaderMap,
935 AxPath((name, coll)): AxPath<(String, String)>,
936) -> Response {
937 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
938 let db = match mgr.get_db(&name).await {
939 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
940 Some(db) => db,
941 };
942 let (seq, head) = db_seq_head(&db);
943 let tip = db.tip_collection(&coll).map(|n| serde_json::to_value(&n).unwrap_or(Value::Null));
944 ok(json!({"coll": coll, "tip": tip, "seq": seq, "head": head}))
945}
946
947#[derive(Deserialize)]
948struct SinceQuery { after_seq: Option<u64>, limit: Option<usize> }
949
950async fn since_database(
951 State(mgr): State<Manager>,
952 headers: HeaderMap,
953 AxPath(name): AxPath<String>,
954 AxQuery(q): AxQuery<SinceQuery>,
955) -> Response {
956 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
957 let db = match mgr.get_db(&name).await {
958 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
959 Some(db) => db,
960 };
961 let after = q.after_seq.unwrap_or(0);
962 let b = db.since(after, q.limit.unwrap_or(0));
963 let nodes: Vec<Value> = b.nodes.iter()
964 .map(|n| serde_json::to_value(n).unwrap_or(Value::Null))
965 .collect();
966 let (seq, head) = db_seq_head(&db);
967 ok(json!({
968 "nodes": nodes, "count": nodes.len(),
969 "from_seq": b.from_seq, "to_seq": b.to_seq, "head_seq": b.head_seq, "has_more": b.has_more,
970 "seq": seq, "head": head
971 }))
972}
973
974async fn status_database(
977 State(mgr): State<Manager>,
978 headers: HeaderMap,
979 AxPath(name): AxPath<String>,
980) -> Response {
981 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
982 let db = match mgr.get_db(&name).await {
983 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
984 Some(db) => db,
985 };
986 let s = db.scan_status();
987 ok(json!({
988 "ok": true,
989 "scan_complete": s.scan_complete,
990 "tip_seq": s.tip_seq,
991 "indexed_seq_min": s.indexed_seq_min,
992 "indexed_seq_max": s.indexed_seq_max,
993 "indexed_count": s.indexed_count
994 }))
995}
996
997#[derive(Deserialize)]
1000struct SubscribeBody { nql: String }
1001
1002async fn subscribe_query(
1003 State(mgr): State<Manager>,
1004 headers: HeaderMap,
1005 AxPath(name): AxPath<String>,
1006 Json(body): Json<SubscribeBody>,
1007) -> Response {
1008 if !mgr.check_auth(&headers) {
1009 return err(StatusCode::UNAUTHORIZED, "unauthorized");
1010 }
1011 let db = match mgr.get_db(&name).await {
1012 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
1013 Some(db) => db,
1014 };
1015
1016 let (sub_id, rx) = mgr.subscribe(&name, body.nql.clone());
1017
1018 if let Ok((rows, _)) = crate::nql::query(&db, &body.nql) {
1020 let init = json!({
1021 "sub_id": sub_id,
1022 "db": &name,
1023 "nql": &body.nql,
1024 "rows": rows,
1025 "count": rows.len(),
1026 "event": "initial",
1027 });
1028 if let Some(mut entry) = mgr.subs.get_mut(&(name.clone(), sub_id)) {
1030 let hash = format!("{:?}", rows);
1031 entry.value_mut().1 = hash;
1032 }
1033 if let Some(entry) = mgr.subs.get(&(name.clone(), sub_id)) {
1035 let _ = entry.value().2.send(init.to_string());
1036 }
1037 }
1038
1039 let stream = BroadcastStream::new(rx).filter_map(|msg| {
1040 match msg {
1041 Ok(line) => Some(Ok::<Event, std::convert::Infallible>(Event::default().data(line))),
1042 Err(_) => None,
1043 }
1044 });
1045 Sse::new(stream)
1046 .keep_alive(KeepAlive::default())
1047 .into_response()
1048}
1049
1050async fn unsubscribe_query(
1051 State(mgr): State<Manager>,
1052 headers: HeaderMap,
1053 AxPath((name, sub_id)): AxPath<(String, u64)>,
1054) -> Response {
1055 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
1056 mgr.unsubscribe(&name, sub_id);
1057 ok(json!({"ok": true, "sub_id": sub_id}))
1058}
1059
1060async fn log_events(State(mgr): State<Manager>) -> Sse<impl futures_core::Stream<Item = Result<Event, std::convert::Infallible>>> {
1063 let rx = mgr.log_tx.subscribe();
1064 let stream = BroadcastStream::new(rx).filter_map(|msg| {
1065 match msg {
1066 Ok(line) => Some(Ok::<Event, std::convert::Infallible>(Event::default().data(line))),
1067 Err(_) => None, }
1069 });
1070 Sse::new(stream).keep_alive(KeepAlive::default())
1071}
1072
1073pub fn router(mgr: Manager) -> Router {
1076 Router::new()
1077 .route("/health", get(health))
1078 .route("/events", get(log_events))
1079 .route("/v1/databases", get(list_databases).post(create_database))
1080 .route("/v1/databases/:name", get(get_database).delete(drop_database))
1081 .route("/v1/databases/:name/query", post(query_database))
1082 .route("/v1/databases/:name/cast", post(cast_prompt))
1083 .route("/v1/databases/:name/put", post(put_document))
1084 .route("/v1/databases/:name/link", post(link_document))
1085 .route("/v1/databases/:name/rows/:coll/:id",
1088 get(get_document).delete(delete_document))
1089 .route("/v1/databases/:name/batch", post(batch_operations))
1090 .route("/v1/databases/:name/index", post(create_index))
1091 .route("/v1/databases/:name/verify", get(verify_database))
1092 .route("/v1/databases/:name/checkpoint", post(checkpoint))
1093 .route("/v1/databases/:name/log", get(get_log))
1094 .route("/v1/databases/:name/tip", get(tip_database))
1095 .route("/v1/databases/:name/collections/:coll/tip", get(tip_collection_database))
1096 .route("/v1/databases/:name/since", get(since_database))
1097 .route("/v1/databases/:name/status", get(status_database))
1098 .route("/v1/databases/:name/subscribe", post(subscribe_query))
1099 .route("/v1/databases/:name/subscribe/:sub_id", delete(unsubscribe_query))
1100 .with_state(mgr)
1101}
1102
1103impl crate::pgwire::DbResolver for Manager {
1108 fn resolve(&self, name: &str) -> Option<Arc<Db>> {
1109 let inner = self.inner.blocking_read();
1112 if name.is_empty() {
1116 if inner.dbs.len() == 1 {
1117 return inner.dbs.values().next().cloned();
1118 }
1119 return None;
1120 }
1121 inner.dbs.get(name).cloned()
1122 }
1123 fn token(&self) -> Option<String> {
1124 self.token.clone()
1125 }
1126}
1127
1128pub async fn run(host: &str, port: u16, data_dir: &str, tmk: Option<[u8; 32]>, token: Option<String>, memory_mode: bool) -> anyhow::Result<()> {
1129 #[cfg(feature = "cast")]
1134 let mut mgr = Manager::new(Path::new(data_dir), tmk, token, memory_mode);
1135 #[cfg(not(feature = "cast"))]
1136 let mgr = Manager::new(Path::new(data_dir), tmk, token, memory_mode);
1137
1138 mgr.open_all().await?;
1139
1140 #[cfg(feature = "cast")]
1144 {
1145 let want = std::env::var("NEDBD_CAST").map(|v| v == "1").unwrap_or(false);
1146 if want {
1147 match crate::cast::Caster::load(Path::new(data_dir)) {
1148 Ok(c) => {
1149 println!(" cast enabled — {:.2}M params, vocab {}, {}",
1150 c.n_params() as f64 / 1e6, c.vocab_size(), c.source());
1151 mgr.caster = Some(c);
1152 }
1153 Err(e) => {
1154 eprintln!(" cast DISABLED — {}", e);
1155 }
1156 }
1157 }
1158 }
1159 #[cfg(feature = "cast")]
1162 let mgr = mgr;
1163
1164 let has_token = mgr.token.is_some();
1165 let mgr_for_shutdown = mgr.clone();
1166 if let Ok(raw) = std::env::var("NEDBD_PG_PORT") {
1171 match raw.trim().parse::<u16>() {
1172 Ok(pg_port) if pg_port > 0 => {
1173 let pg_host = host.to_string();
1174 let resolver: Arc<dyn crate::pgwire::DbResolver> = Arc::new(mgr.clone());
1175 tokio::spawn(async move {
1176 if let Err(e) = crate::pgwire::run(&pg_host, pg_port, resolver).await {
1177 eprintln!(" [pgwire] listener stopped: {}", e);
1178 }
1179 });
1180 }
1181 _ => eprintln!(" [pgwire] ignoring NEDBD_PG_PORT={:?} — not a valid port", raw),
1182 }
1183 }
1184
1185 let app = router(mgr);
1186 let addr = format!("{}:{}", host, port).parse::<std::net::SocketAddr>()?;
1187 let banner = format!(r#"
1188 ◆
1189 ╱ ╲ N E D B · DAG ENGINE {}
1190 ◆ ◆ ─────────────────────────────────────────────
1191 ╱ ╲ ╱ ╲ content-addressed · tamper-evident · causal
1192 ◆ ◆ ◆ bi-temporal · replay-protected · encrypted
1193 ╱ ╲ ╱ ╲ ╱ ╲
1194 ◆ ◆ ◆ ◆ © INTERCHAINED LLC × Vex (Interchained AI fleet: GLM · Claude · Opus · Fable · GPT-6)
1195 ╱ ╲ ╱ ╲ ╱ ╲ ╱ ╲ interchained.org · hyperagent.com/refer/J2G6TCD7
1196
1197 ─────────────────────────────────────────────────────────────
1198 listen http://{}
1199 data {}
1200 enc {}
1201 token {}
1202 memory {}
1203 ─────────────────────────────────────────────────────────────
1204"#,
1205 env!("CARGO_PKG_VERSION"),
1206 addr,
1207 data_dir,
1208 if tmk.is_some() { "AES-256-GCM" } else { "off" },
1209 if has_token { "on" } else { "off (set NEDBD_TOKEN to require auth)" },
1210 if memory_mode { "yes — all data lost on exit (NEDBD_MEMORY=1)" } else { "no — durable DAG on disk" }
1211 );
1212 print!("{}", banner);
1213
1214 let listener = tokio::net::TcpListener::bind(addr).await?;
1215
1216 let mgr_hourly = mgr_for_shutdown.clone();
1220 tokio::spawn(async move {
1221 loop {
1222 let now_secs = std::time::SystemTime::now()
1224 .duration_since(std::time::UNIX_EPOCH)
1225 .map(|d| d.as_secs()).unwrap_or(0);
1226 let secs_into_hour = now_secs % 3600;
1227 let sleep_secs = 3600 - secs_into_hour;
1228 tokio::time::sleep(tokio::time::Duration::from_secs(sleep_secs)).await;
1229 mgr_hourly.flush_all().await;
1230 println!(" [nedbd] hourly checkpoint — manifests flushed");
1231 }
1232 });
1233
1234 let shutdown = async {
1236 #[cfg(unix)]
1237 {
1238 use tokio::signal::unix::{signal, SignalKind};
1239 let mut sigterm = signal(SignalKind::terminate()).unwrap();
1240 let mut sigint = signal(SignalKind::interrupt()).unwrap();
1241 tokio::select! {
1242 _ = sigterm.recv() => println!(" [nedbd] SIGTERM — flushing and exiting..."),
1243 _ = sigint.recv() => println!(" [nedbd] SIGINT — flushing and exiting..."),
1244 }
1245 }
1246 #[cfg(not(unix))]
1247 {
1248 tokio::signal::ctrl_c().await.ok();
1249 println!(" [nedbd] shutting down — flushing manifests...");
1250 }
1251 };
1252
1253 axum::serve(listener, app)
1254 .tcp_nodelay(true)
1255 .with_graceful_shutdown(shutdown)
1256 .await?;
1257
1258 mgr_for_shutdown.flush_all().await;
1260 println!(" [nedbd] goodbye");
1261 Ok(())
1262}