1use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::sync::atomic::AtomicU64;
13
14use axum::{
15 extract::{Path as AxPath, State, Query as AxQuery},
16 http::{HeaderMap, StatusCode},
17 response::{IntoResponse, Response, sse::{Event, KeepAlive, Sse}},
18 routing::{delete, get, post},
19 Json, Router,
20};
21use dashmap::DashMap;
22use serde::Deserialize;
23use serde_json::{json, Value};
24use tokio::sync::{broadcast, RwLock};
25use tokio_stream::wrappers::BroadcastStream;
26use tokio_stream::StreamExt as _;
27
28use crate::db::Db;
29use crate::nql;
30use crate::store::Node;
31
32const LOG_CHANNEL_CAP: usize = 512;
35const SUB_CHANNEL_CAP: usize = 256;
36
37type SubKey = (String, u64); type SubVal = (String, String, broadcast::Sender<String>); macro_rules! nlog {
47 ($tx:expr, $($arg:tt)*) => {{
48 let line = format!($($arg)*);
49 println!("{}", line);
50 let _ = $tx.send(line);
51 }};
52}
53
54#[derive(Clone)]
57pub struct Manager {
58 inner: Arc<RwLock<ManagerInner>>,
59 pub token: Option<String>,
60 pub log_tx: broadcast::Sender<String>,
62 subs: Arc<DashMap<SubKey, SubVal>>,
64 sub_ctr: Arc<AtomicU64>,
65 #[cfg(feature = "cast")]
69 pub caster: Option<crate::cast::Caster>,
70}
71
72struct ManagerInner {
73 data_dir: PathBuf,
74 dbs: HashMap<String, Arc<Db>>,
75 tmk: Option<[u8; 32]>,
76 memory_mode: bool,
77}
78
79impl Manager {
80 pub fn new(data_dir: &Path, tmk: Option<[u8; 32]>, token: Option<String>, memory_mode: bool) -> Self {
81 let (log_tx, _) = broadcast::channel(LOG_CHANNEL_CAP);
82 Self {
83 inner: Arc::new(RwLock::new(ManagerInner {
84 data_dir: data_dir.to_path_buf(),
85 dbs: HashMap::new(),
86 tmk,
87 memory_mode,
88 })),
89 token,
90 log_tx,
91 #[cfg(feature = "cast")]
92 caster: None,
93 subs: Arc::new(DashMap::new()),
94 sub_ctr: Arc::new(AtomicU64::new(1)),
95 }
96 }
97
98 fn subscribe(&self, db: &str, nql: String) -> (u64, broadcast::Receiver<String>) {
100 use std::sync::atomic::Ordering;
101 let id = self.sub_ctr.fetch_add(1, Ordering::Relaxed);
102 let (tx, rx) = broadcast::channel(SUB_CHANNEL_CAP);
103 self.subs.insert((db.to_string(), id), (nql, String::new(), tx));
104 (id, rx)
105 }
106
107 fn unsubscribe(&self, db: &str, sub_id: u64) {
109 self.subs.remove(&(db.to_string(), sub_id));
110 }
111
112 fn notify_subscribers(&self, db: &str, db_arc: &Arc<crate::db::Db>) {
114 let keys: Vec<SubKey> = self.subs.iter()
115 .filter(|e| e.key().0 == db)
116 .map(|e| e.key().clone())
117 .collect();
118
119 for key in keys {
120 if let Some(mut entry) = self.subs.get_mut(&key) {
121 let (nql, last_hash, tx) = entry.value_mut();
122 let rows = match crate::nql::query(db_arc, nql) {
124 Ok((rows, _)) => rows,
125 Err(_) => continue,
126 };
127 let new_hash = format!("{:?}", rows.iter().map(|r| r.to_string()).collect::<Vec<_>>());
129 if new_hash == *last_hash { continue; }
130 *last_hash = new_hash;
131 let event = json!({
133 "sub_id": key.1,
134 "db": &key.0,
135 "nql": nql.as_str(),
136 "rows": rows,
137 "count": rows.len(),
138 });
139 let _ = tx.send(event.to_string());
140 }
141 }
142 }
143
144 pub async fn open_all(&self) -> anyhow::Result<()> {
146 let (data_dir, tmk, memory_mode) = {
147 let inner = self.inner.read().await;
148 (inner.data_dir.clone(), inner.tmk, inner.memory_mode)
149 };
150 if memory_mode { return Ok(()); }
152 if !data_dir.exists() {
153 std::fs::create_dir_all(&data_dir)?;
154 return Ok(());
155 }
156 let mut names = vec![];
157 for entry in std::fs::read_dir(&data_dir)? {
158 let entry = entry?;
159 if entry.file_type()?.is_dir() {
160 names.push(entry.file_name().to_string_lossy().to_string());
161 }
162 }
163 let log_tx = self.log_tx.clone();
164 let mut inner = self.inner.write().await;
165 for name in names {
166 let db_path = inner.data_dir.join(&name);
167 let dek = tmk.map(|k| crate::store::Dek::from_tmk(&k, name.as_bytes()));
168 match Db::open(&db_path, dek) {
169 Ok(db) => {
170 nlog!(log_tx, " [nedbd] opened database {:?}", name);
171 let db_arc = Arc::new(db);
172 Db::start_cold_scan(Arc::clone(&db_arc));
173 Db::start_manifest_ticker(Arc::clone(&db_arc), 1000);
175 inner.dbs.insert(name, db_arc);
176 }
177 Err(e) => nlog!(log_tx, " [nedbd] ERROR opening {:?}: {}", name, e),
178 }
179 }
180 Ok(())
181 }
182
183 async fn get_db(&self, name: &str) -> Option<Arc<Db>> {
184 self.inner.read().await.dbs.get(name).cloned()
185 }
186
187 async fn create_db(&self, name: &str) -> anyhow::Result<Arc<Db>> {
188 let (data_dir, tmk, memory_mode) = {
189 let inner = self.inner.read().await;
190 (inner.data_dir.clone(), inner.tmk, inner.memory_mode)
191 };
192 let db = if memory_mode {
193 Arc::new(Db::in_memory())
195 } else {
196 let db_path = data_dir.join(name);
197 let dek = tmk.map(|k| crate::store::Dek::from_tmk(&k, name.as_bytes()));
198 let db = Arc::new(Db::open(&db_path, dek)?);
199 Db::start_cold_scan(Arc::clone(&db));
200 Db::start_manifest_ticker(Arc::clone(&db), 1000);
201 db
202 };
203 self.inner.write().await.dbs.insert(name.to_string(), db.clone());
204 Ok(db)
205 }
206
207 async fn drop_db(&self, name: &str) -> bool {
208 let db = self.inner.write().await.dbs.remove(name);
209 if let Some(db) = db {
210 db.flush_manifest_if_dirty();
212 let data_dir = self.inner.read().await.data_dir.clone();
213 let _ = std::fs::remove_dir_all(data_dir.join(name));
214 true
215 } else {
216 false
217 }
218 }
219
220 pub async fn flush_all(&self) {
222 let inner = self.inner.read().await;
223 for db in inner.dbs.values() {
224 db.flush_all(); }
226 }
227
228 async fn names(&self) -> Vec<String> {
229 self.inner.read().await.dbs.keys().cloned().collect()
230 }
231
232 pub fn log(&self, msg: impl Into<String>) {
234 let line = msg.into();
235 println!("{}", line);
236 let _ = self.log_tx.send(line);
237 }
238
239 fn check_auth(&self, headers: &HeaderMap) -> bool {
240 match &self.token {
241 None => true,
242 Some(required) => {
243 if let Some(auth) = headers.get("authorization") {
244 if let Ok(s) = auth.to_str() {
245 return s == format!("Bearer {}", required);
246 }
247 }
248 false
249 }
250 }
251 }
252}
253
254fn err(status: StatusCode, msg: &str) -> Response {
257 (status, Json(json!({"error": msg}))).into_response()
258}
259
260fn ok(body: Value) -> Response {
261 (StatusCode::OK, Json(body)).into_response()
262}
263
264fn db_seq_head(db: &Db) -> (u64, String) {
268 let seq = db.seq.load(std::sync::atomic::Ordering::SeqCst);
269 let head = db.head();
270 (seq, head)
271}
272
273async fn health(State(mgr): State<Manager>) -> Response {
276 let names = mgr.names().await;
277 let inner = mgr.inner.read().await;
278 ok(json!({
279 "ok": true,
280 "service": "nedbd",
281 "version": env!("CARGO_PKG_VERSION"),
282 "engine": "dag",
283 "memory": inner.memory_mode,
284 "databases": names,
285 "encrypted": inner.tmk.is_some(),
286 }))
287}
288
289async fn list_databases(State(mgr): State<Manager>, headers: HeaderMap) -> Response {
290 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
291 let names = mgr.names().await;
292 let summaries: Vec<Value> = {
293 let inner = mgr.inner.read().await;
294 names.iter().map(|n| {
295 if let Some(db) = inner.dbs.get(n) {
296 let (seq, head) = db_seq_head(db);
297 json!({"name": n, "seq": seq, "head": head, "collections": db.id_index.collections()})
298 } else {
299 json!({"name": n})
300 }
301 }).collect()
302 };
303 ok(json!({"databases": summaries}))
304}
305
306#[derive(Deserialize)]
307struct CreateDbBody { name: String }
308
309async fn create_database(
310 State(mgr): State<Manager>,
311 headers: HeaderMap,
312 Json(body): Json<CreateDbBody>,
313) -> Response {
314 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
315 if body.name.is_empty() { return err(StatusCode::BAD_REQUEST, "name is required"); }
316 match mgr.create_db(&body.name).await {
317 Ok(db) => {
318 let (seq, head) = db_seq_head(&db);
319 (StatusCode::CREATED, Json(json!({"database": {"name": body.name, "seq": seq, "head": head}}))).into_response()
320 }
321 Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
322 }
323}
324
325async fn get_database(
326 State(mgr): State<Manager>,
327 headers: HeaderMap,
328 AxPath(name): AxPath<String>,
329) -> Response {
330 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
331 match mgr.get_db(&name).await {
332 None => err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
333 Some(db) => {
334 let (seq, head) = db_seq_head(&db);
335 ok(json!({"name": name, "seq": seq, "head": head, "collections": db.id_index.collections()}))
336 }
337 }
338}
339
340async fn drop_database(
341 State(mgr): State<Manager>,
342 headers: HeaderMap,
343 AxPath(name): AxPath<String>,
344) -> Response {
345 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
346 let dropped = mgr.drop_db(&name).await;
347 ok(json!({"dropped": dropped}))
348}
349
350#[derive(Deserialize)]
351struct QueryBody { nql: String }
352
353#[derive(Deserialize)]
356struct CastBody {
357 prompt: String,
358 #[serde(default)]
362 execute: bool,
363}
364
365#[cfg(feature = "cast")]
371async fn cast_prompt(
372 State(mgr): State<Manager>,
373 headers: HeaderMap,
374 AxPath(name): AxPath<String>,
375 Json(body): Json<CastBody>,
376) -> Response {
377 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
378
379 let caster = match &mgr.caster {
380 Some(c) => c,
381 None => return err(
382 StatusCode::SERVICE_UNAVAILABLE,
383 "cast is not enabled; start nedbd with --cast (or NEDBD_CAST=1) \
384 and place model.cast in the data directory",
385 ),
386 };
387
388 let db = match mgr.get_db(&name).await {
389 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
390 Some(db) => db,
391 };
392 if body.prompt.trim().is_empty() {
393 return err(StatusCode::BAD_REQUEST, "prompt is required");
394 }
395
396 let collections = db.id_index.collections();
399 let result = caster.cast_checked(&body.prompt, &collections);
400
401 let parse_err = match nql::parse(&result.nql) {
404 Ok(_) => None,
405 Err(e) => Some(e.to_string()),
406 };
407
408 let (seq, head) = db_seq_head(&db);
409 let mut out = json!({
410 "prompt": body.prompt,
411 "nql": result.nql,
412 "valid": parse_err.is_none(),
413 "collection": result.collection,
414 "collection_known": result.collection_known,
415 "collections": collections,
416 "executed": false,
417 "seq": seq,
418 "head": head,
419 });
420
421 if let Some(e) = parse_err {
422 out["error"] = json!(format!("NQL error: {}", e));
425 return (StatusCode::UNPROCESSABLE_ENTITY, Json(out)).into_response();
426 }
427
428 if !result.collection_known {
429 out["error"] = json!(format!(
433 "collection {:?} does not exist in {:?}",
434 result.collection.unwrap_or_default(), name
435 ));
436 return (StatusCode::UNPROCESSABLE_ENTITY, Json(out)).into_response();
437 }
438
439 if !body.execute {
440 return ok(out);
441 }
442
443 let nql_text = out["nql"].as_str().unwrap_or("").to_string();
445 match nql::query(&db, &nql_text) {
446 Ok((rows, count)) => {
447 out["executed"] = json!(true);
448 out["rows"] = json!(rows);
449 out["count"] = json!(count);
450 ok(out)
451 }
452 Err(e) => {
453 out["error"] = json!(format!("NQL error: {}", e));
454 (StatusCode::BAD_REQUEST, Json(out)).into_response()
455 }
456 }
457}
458
459#[cfg(not(feature = "cast"))]
462async fn cast_prompt(
463 State(_mgr): State<Manager>,
464 _headers: HeaderMap,
465 AxPath(_name): AxPath<String>,
466 Json(_body): Json<CastBody>,
467) -> Response {
468 err(
469 StatusCode::NOT_IMPLEMENTED,
470 "this nedbd was built without the `cast` feature; \
471 rebuild with --features cast to enable natural-language planning",
472 )
473}
474
475async fn query_database(
476 State(mgr): State<Manager>,
477 headers: HeaderMap,
478 AxPath(name): AxPath<String>,
479 Json(body): Json<QueryBody>,
480) -> Response {
481 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
482 let db = match mgr.get_db(&name).await {
483 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
484 Some(db) => db,
485 };
486 if body.nql.trim().is_empty() {
487 return err(StatusCode::BAD_REQUEST, "nql is required");
488 }
489 match nql::query(&db, &body.nql) {
490 Ok((rows, count)) => {
491 let (seq, head) = db_seq_head(&db);
492 ok(json!({"rows": rows, "count": count, "seq": seq, "head": head}))
493 }
494 Err(e) => err(StatusCode::BAD_REQUEST, &format!("NQL error: {}", e)),
495 }
496}
497
498#[derive(Deserialize)]
499struct PutBody {
500 coll: String,
501 id: String,
502 doc: Value,
503 caused_by: Option<Vec<serde_json::Value>>,
504 valid_from: Option<String>,
505 valid_to: Option<String>,
506 #[allow(dead_code)] evidence: Option<String>,
507 #[allow(dead_code)] confidence: Option<f64>,
508 #[allow(dead_code)] client: Option<String>,
509 #[allow(dead_code)] nonce: Option<u64>,
510 #[allow(dead_code)] idem: Option<String>,
511}
512
513#[derive(Deserialize)]
514struct LinkBody {
515 frm: String,
516 rel: String,
517 to: String,
518}
519
520async fn put_document(
521 State(mgr): State<Manager>,
522 headers: HeaderMap,
523 AxPath(name): AxPath<String>,
524 Json(body): Json<PutBody>,
525) -> Response {
526 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
527 let db = match mgr.get_db(&name).await {
528 None => {
529 match mgr.create_db(&name).await {
531 Ok(db) => db,
532 Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
533 }
534 }
535 Some(db) => db,
536 };
537 if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
540 return err(StatusCode::SERVICE_UNAVAILABLE,
541 "database startup in progress — reads available, writes retry in a moment");
542 }
543 let caused_by: Vec<String> = body.caused_by.unwrap_or_default()
545 .into_iter()
546 .filter_map(|v| match v {
547 serde_json::Value::String(s) => Some(s),
548 serde_json::Value::Number(n) => {
549 n.as_u64().and_then(|seq| db.get_hash_by_seq(seq))
550 }
551 _ => None,
552 })
553 .collect();
554 let coll = body.coll.clone();
557 let id = body.id.clone();
558 let doc = body.doc.clone();
559 let vf = body.valid_from.clone();
560 let vt = body.valid_to.clone();
561 let db2 = Arc::clone(&db);
562 let result = tokio::task::spawn_blocking(move || {
563 db2.put(&coll, &id, doc, caused_by, vf, vt)
564 }).await;
565 match result {
566 Err(join_err) => err(StatusCode::INTERNAL_SERVER_ERROR, &join_err.to_string()),
567 Ok(Err(e)) => err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
568 Ok(Ok(node)) => {
569 let (seq, head) = db_seq_head(&db);
570 mgr.notify_subscribers(&name, &db);
571 ok(json!({"ok": true, "doc": node_to_response(&node), "seq": seq, "head": head}))
572 }
573 }
574}
575
576fn node_to_response(node: &Node) -> Value {
577 json!({
578 "_id": node.id,
579 "_hash": node.hash,
580 "_seq": node.seq,
581 "_coll": node.coll,
582 "data": node.data,
583 })
584}
585
586async fn link_document(
587 State(mgr): State<Manager>,
588 headers: HeaderMap,
589 AxPath(name): AxPath<String>,
590 Json(body): Json<LinkBody>,
591) -> Response {
592 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
593 let db = match mgr.get_db(&name).await {
594 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
595 Some(db) => db,
596 };
597 if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
598 return err(StatusCode::SERVICE_UNAVAILABLE, "startup scan in progress");
599 }
600 match db.link(&body.frm, &body.rel, &body.to) {
601 Ok(()) => {
602 let (seq, head) = db_seq_head(&db);
603 ok(json!({"ok": true, "frm": body.frm, "rel": body.rel, "to": body.to, "seq": seq, "head": head}))
604 }
605 Err(e) => err(StatusCode::BAD_REQUEST, &e.to_string()),
606 }
607}
608
609async fn delete_document(
610 State(mgr): State<Manager>,
611 headers: HeaderMap,
612 AxPath((name, coll, id)): AxPath<(String, String, String)>,
613) -> Response {
614 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
615 let db = match mgr.get_db(&name).await {
616 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
617 Some(db) => db,
618 };
619 let existed = match db.delete(&coll, &id) {
622 Ok(v) => v,
623 Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
624 };
625 let (seq, head) = db_seq_head(&db);
626 ok(json!({"ok": existed, "seq": seq, "head": head}))
627}
628
629#[derive(Deserialize)]
630struct BatchOp {
631 op: String,
632 coll: Option<String>,
633 id: Option<String>,
634 doc: Option<Value>,
635 caused_by: Option<Vec<serde_json::Value>>,
636}
637#[derive(Deserialize)]
638struct BatchBody { ops: Vec<BatchOp> }
639
640async fn batch_operations(
641 State(mgr): State<Manager>,
642 headers: HeaderMap,
643 AxPath(name): AxPath<String>,
644 Json(body): Json<BatchBody>,
645) -> Response {
646 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
647 let db = match mgr.get_db(&name).await {
648 None => match mgr.create_db(&name).await {
649 Ok(db) => db,
650 Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
651 },
652 Some(db) => db,
653 };
654
655 if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
656 return err(StatusCode::SERVICE_UNAVAILABLE,
657 "database startup in progress — reads available, writes retry in a moment");
658 }
659
660 let mut put_ops = vec![];
664 let mut del_ops: Vec<(String, String)> = vec![];
665 let mut op_order: Vec<(&str, usize)> = vec![]; for op in &body.ops {
668 let t = op.op.to_lowercase();
669 match t.as_str() {
670 "put" => {
671 let caused_by: Vec<String> = op.caused_by.clone().unwrap_or_default()
673 .into_iter()
674 .filter_map(|v| match v {
675 serde_json::Value::String(s) => Some(s),
676 serde_json::Value::Number(n) => {
677 n.as_u64().and_then(|seq| db.get_hash_by_seq(seq))
678 }
679 _ => None,
680 })
681 .collect();
682 op_order.push(("put", put_ops.len()));
683 put_ops.push((
684 op.coll.clone().unwrap_or_default(),
685 op.id.clone().unwrap_or_default(),
686 op.doc.clone().unwrap_or(json!({})),
687 caused_by,
688 None::<String>,
689 None::<String>,
690 ));
691 }
692 "del" | "delete" => {
693 op_order.push(("del", del_ops.len()));
694 del_ops.push((
695 op.coll.clone().unwrap_or_default(),
696 op.id.clone().unwrap_or_default(),
697 ));
698 }
699 _ => { op_order.push(("unknown", 0)); }
700 }
701 }
702
703 let put_results = if put_ops.is_empty() {
705 vec![]
706 } else {
707 match db.put_batch(put_ops) {
708 Ok(nodes) => nodes.into_iter().map(|n| json!({"op":"put","id":n.id,"seq":n.seq,"hash":n.hash})).collect(),
709 Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
710 }
711 };
712
713 let del_results: Vec<serde_json::Value> = del_ops.iter().map(|(coll, id)| {
715 match db.delete(coll, id) {
716 Ok(existed) => json!({"op":"del","id":id,"ok":existed}),
717 Err(e) => json!({"op":"del","id":id,"error":e.to_string()}),
718 }
719 }).collect();
720
721 let mut results = vec![];
723 for (kind, idx) in &op_order {
724 let r = match *kind {
725 "put" => put_results.get(*idx).cloned().unwrap_or(json!({"op":"put","error":"missing"})),
726 "del" => del_results.get(*idx).cloned().unwrap_or(json!({"op":"del","error":"missing"})),
727 _ => json!({"op": kind, "error": "unknown op"}),
728 };
729 results.push(r);
730 }
731 let (seq, head) = db_seq_head(&db);
732 mgr.notify_subscribers(&name, &db);
734 ok(json!({"results": results, "count": results.len(), "seq": seq, "head": head}))
735}
736
737#[derive(Deserialize)]
738struct IndexBody { coll: String, field: String, kind: Option<String> }
739
740async fn create_index(
741 State(mgr): State<Manager>,
742 headers: HeaderMap,
743 AxPath(name): AxPath<String>,
744 Json(body): Json<IndexBody>,
745) -> Response {
746 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
747 let db = match mgr.get_db(&name).await {
748 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
749 Some(db) => db,
750 };
751 let kind = body.kind.as_deref().unwrap_or("eq");
752 match kind {
753 "sorted" | "eq" => {
754 db.create_sorted_index(&body.coll, &body.field);
755 ok(json!({"ok": true, "coll": body.coll, "field": body.field, "kind": kind}))
756 }
757 _ => err(StatusCode::BAD_REQUEST, &format!("unknown index kind: {}", kind)),
758 }
759}
760
761async fn verify_database(
762 State(mgr): State<Manager>,
763 headers: HeaderMap,
764 AxPath(name): AxPath<String>,
765) -> Response {
766 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
767 let db = match mgr.get_db(&name).await {
768 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
769 Some(db) => db,
770 };
771 let (ok_count, tampered) = db.verify();
772 let (seq, head) = db_seq_head(&db);
773 ok(json!({
774 "ok": tampered.is_empty(),
775 "seq": seq,
776 "head": head,
777 "tamper_evident": true,
778 "objects_checked": ok_count,
779 "tampered": tampered,
780 }))
781}
782
783async fn checkpoint(
784 State(mgr): State<Manager>,
785 headers: HeaderMap,
786 AxPath(name): AxPath<String>,
787) -> Response {
788 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
789 let db = match mgr.get_db(&name).await {
790 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
791 Some(db) => db,
792 };
793 let (seq, head) = db_seq_head(&db);
794 ok(json!({"ok": true, "head": head, "seq": seq}))
796}
797
798#[derive(Deserialize)]
799struct LogQuery { limit: Option<usize> }
800
801async fn get_log(
802 State(mgr): State<Manager>,
803 headers: HeaderMap,
804 AxPath(name): AxPath<String>,
805 AxQuery(q): AxQuery<LogQuery>,
806) -> Response {
807 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
808 let db = match mgr.get_db(&name).await {
809 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
810 Some(db) => db,
811 };
812 let limit = q.limit.unwrap_or(50);
813 let mut log_entries: Vec<Value> = db.objects.all_hashes()
815 .filter_map(|h| db.objects.read(&h).ok())
816 .take(limit)
817 .map(|n| json!({
818 "seq": n.seq, "coll": n.coll, "id": n.id,
819 "hash": n.hash, "ts": n.ts, "op": "put"
820 }))
821 .collect();
822 log_entries.sort_by(|a, b|
823 b["seq"].as_u64().cmp(&a["seq"].as_u64())
824 );
825 log_entries.truncate(limit);
826 let (seq, head) = db_seq_head(&db);
827 ok(json!({"log": log_entries, "seq": seq, "head": head}))
828}
829
830async fn tip_database(
836 State(mgr): State<Manager>,
837 headers: HeaderMap,
838 AxPath(name): AxPath<String>,
839) -> Response {
840 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
841 let db = match mgr.get_db(&name).await {
842 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
843 Some(db) => db,
844 };
845 let (seq, head) = db_seq_head(&db);
846 let tip = db.tip().map(|n| serde_json::to_value(&n).unwrap_or(Value::Null));
847 ok(json!({"tip": tip, "seq": seq, "head": head}))
848}
849
850async fn tip_collection_database(
852 State(mgr): State<Manager>,
853 headers: HeaderMap,
854 AxPath((name, coll)): AxPath<(String, String)>,
855) -> Response {
856 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
857 let db = match mgr.get_db(&name).await {
858 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
859 Some(db) => db,
860 };
861 let (seq, head) = db_seq_head(&db);
862 let tip = db.tip_collection(&coll).map(|n| serde_json::to_value(&n).unwrap_or(Value::Null));
863 ok(json!({"coll": coll, "tip": tip, "seq": seq, "head": head}))
864}
865
866#[derive(Deserialize)]
867struct SinceQuery { after_seq: Option<u64>, limit: Option<usize> }
868
869async fn since_database(
870 State(mgr): State<Manager>,
871 headers: HeaderMap,
872 AxPath(name): AxPath<String>,
873 AxQuery(q): AxQuery<SinceQuery>,
874) -> Response {
875 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
876 let db = match mgr.get_db(&name).await {
877 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
878 Some(db) => db,
879 };
880 let after = q.after_seq.unwrap_or(0);
881 let b = db.since(after, q.limit.unwrap_or(0));
882 let nodes: Vec<Value> = b.nodes.iter()
883 .map(|n| serde_json::to_value(n).unwrap_or(Value::Null))
884 .collect();
885 let (seq, head) = db_seq_head(&db);
886 ok(json!({
887 "nodes": nodes, "count": nodes.len(),
888 "from_seq": b.from_seq, "to_seq": b.to_seq, "head_seq": b.head_seq, "has_more": b.has_more,
889 "seq": seq, "head": head
890 }))
891}
892
893async fn status_database(
896 State(mgr): State<Manager>,
897 headers: HeaderMap,
898 AxPath(name): AxPath<String>,
899) -> Response {
900 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
901 let db = match mgr.get_db(&name).await {
902 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
903 Some(db) => db,
904 };
905 let s = db.scan_status();
906 ok(json!({
907 "ok": true,
908 "scan_complete": s.scan_complete,
909 "tip_seq": s.tip_seq,
910 "indexed_seq_min": s.indexed_seq_min,
911 "indexed_seq_max": s.indexed_seq_max,
912 "indexed_count": s.indexed_count
913 }))
914}
915
916#[derive(Deserialize)]
919struct SubscribeBody { nql: String }
920
921async fn subscribe_query(
922 State(mgr): State<Manager>,
923 headers: HeaderMap,
924 AxPath(name): AxPath<String>,
925 Json(body): Json<SubscribeBody>,
926) -> Response {
927 if !mgr.check_auth(&headers) {
928 return err(StatusCode::UNAUTHORIZED, "unauthorized");
929 }
930 let db = match mgr.get_db(&name).await {
931 None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
932 Some(db) => db,
933 };
934
935 let (sub_id, rx) = mgr.subscribe(&name, body.nql.clone());
936
937 if let Ok((rows, _)) = crate::nql::query(&db, &body.nql) {
939 let init = json!({
940 "sub_id": sub_id,
941 "db": &name,
942 "nql": &body.nql,
943 "rows": rows,
944 "count": rows.len(),
945 "event": "initial",
946 });
947 if let Some(mut entry) = mgr.subs.get_mut(&(name.clone(), sub_id)) {
949 let hash = format!("{:?}", rows);
950 entry.value_mut().1 = hash;
951 }
952 if let Some(entry) = mgr.subs.get(&(name.clone(), sub_id)) {
954 let _ = entry.value().2.send(init.to_string());
955 }
956 }
957
958 let stream = BroadcastStream::new(rx).filter_map(|msg| {
959 match msg {
960 Ok(line) => Some(Ok::<Event, std::convert::Infallible>(Event::default().data(line))),
961 Err(_) => None,
962 }
963 });
964 Sse::new(stream)
965 .keep_alive(KeepAlive::default())
966 .into_response()
967}
968
969async fn unsubscribe_query(
970 State(mgr): State<Manager>,
971 headers: HeaderMap,
972 AxPath((name, sub_id)): AxPath<(String, u64)>,
973) -> Response {
974 if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
975 mgr.unsubscribe(&name, sub_id);
976 ok(json!({"ok": true, "sub_id": sub_id}))
977}
978
979async fn log_events(State(mgr): State<Manager>) -> Sse<impl futures_core::Stream<Item = Result<Event, std::convert::Infallible>>> {
982 let rx = mgr.log_tx.subscribe();
983 let stream = BroadcastStream::new(rx).filter_map(|msg| {
984 match msg {
985 Ok(line) => Some(Ok::<Event, std::convert::Infallible>(Event::default().data(line))),
986 Err(_) => None, }
988 });
989 Sse::new(stream).keep_alive(KeepAlive::default())
990}
991
992pub fn router(mgr: Manager) -> Router {
995 Router::new()
996 .route("/health", get(health))
997 .route("/events", get(log_events))
998 .route("/v1/databases", get(list_databases).post(create_database))
999 .route("/v1/databases/:name", get(get_database).delete(drop_database))
1000 .route("/v1/databases/:name/query", post(query_database))
1001 .route("/v1/databases/:name/cast", post(cast_prompt))
1002 .route("/v1/databases/:name/put", post(put_document))
1003 .route("/v1/databases/:name/link", post(link_document))
1004 .route("/v1/databases/:name/rows/:coll/:id", delete(delete_document))
1005 .route("/v1/databases/:name/batch", post(batch_operations))
1006 .route("/v1/databases/:name/index", post(create_index))
1007 .route("/v1/databases/:name/verify", get(verify_database))
1008 .route("/v1/databases/:name/checkpoint", post(checkpoint))
1009 .route("/v1/databases/:name/log", get(get_log))
1010 .route("/v1/databases/:name/tip", get(tip_database))
1011 .route("/v1/databases/:name/collections/:coll/tip", get(tip_collection_database))
1012 .route("/v1/databases/:name/since", get(since_database))
1013 .route("/v1/databases/:name/status", get(status_database))
1014 .route("/v1/databases/:name/subscribe", post(subscribe_query))
1015 .route("/v1/databases/:name/subscribe/:sub_id", delete(unsubscribe_query))
1016 .with_state(mgr)
1017}
1018
1019pub async fn run(host: &str, port: u16, data_dir: &str, tmk: Option<[u8; 32]>, token: Option<String>, memory_mode: bool) -> anyhow::Result<()> {
1021 let mut mgr = Manager::new(Path::new(data_dir), tmk, token, memory_mode);
1022 mgr.open_all().await?;
1023
1024 #[cfg(feature = "cast")]
1028 {
1029 let want = std::env::var("NEDBD_CAST").map(|v| v == "1").unwrap_or(false);
1030 if want {
1031 match crate::cast::Caster::load(Path::new(data_dir)) {
1032 Ok(c) => {
1033 println!(" cast enabled — {:.2}M params, vocab {}, {}",
1034 c.n_params() as f64 / 1e6, c.vocab_size(), c.source());
1035 mgr.caster = Some(c);
1036 }
1037 Err(e) => {
1038 eprintln!(" cast DISABLED — {}", e);
1039 }
1040 }
1041 }
1042 }
1043 let mgr = mgr;
1044
1045 let has_token = mgr.token.is_some();
1046 let mgr_for_shutdown = mgr.clone();
1047 let app = router(mgr);
1048 let addr = format!("{}:{}", host, port).parse::<std::net::SocketAddr>()?;
1049 let banner = format!(r#"
1050 ◆
1051 ╱ ╲ N E D B · DAG ENGINE {}
1052 ◆ ◆ ─────────────────────────────────────────────
1053 ╱ ╲ ╱ ╲ content-addressed · tamper-evident · causal
1054 ◆ ◆ ◆ bi-temporal · replay-protected · encrypted
1055 ╱ ╲ ╱ ╲ ╱ ╲
1056 ◆ ◆ ◆ ◆ © INTERCHAINED, LLC × Vex (Claude Sonnet 4.6)
1057 ╱ ╲ ╱ ╲ ╱ ╲ ╱ ╲ interchained.org · hyperagent.com/refer/J2G6TCD7
1058
1059 ─────────────────────────────────────────────────────────────
1060 listen http://{}
1061 data {}
1062 enc {}
1063 token {}
1064 memory {}
1065 ─────────────────────────────────────────────────────────────
1066"#,
1067 env!("CARGO_PKG_VERSION"),
1068 addr,
1069 data_dir,
1070 if tmk.is_some() { "AES-256-GCM" } else { "off" },
1071 if has_token { "on" } else { "off (set NEDBD_TOKEN to require auth)" },
1072 if memory_mode { "yes — all data lost on exit (NEDBD_MEMORY=1)" } else { "no — durable DAG on disk" }
1073 );
1074 print!("{}", banner);
1075
1076 let listener = tokio::net::TcpListener::bind(addr).await?;
1077
1078 let mgr_hourly = mgr_for_shutdown.clone();
1082 tokio::spawn(async move {
1083 loop {
1084 let now_secs = std::time::SystemTime::now()
1086 .duration_since(std::time::UNIX_EPOCH)
1087 .map(|d| d.as_secs()).unwrap_or(0);
1088 let secs_into_hour = now_secs % 3600;
1089 let sleep_secs = 3600 - secs_into_hour;
1090 tokio::time::sleep(tokio::time::Duration::from_secs(sleep_secs)).await;
1091 mgr_hourly.flush_all().await;
1092 println!(" [nedbd] hourly checkpoint — manifests flushed");
1093 }
1094 });
1095
1096 let shutdown = async {
1098 #[cfg(unix)]
1099 {
1100 use tokio::signal::unix::{signal, SignalKind};
1101 let mut sigterm = signal(SignalKind::terminate()).unwrap();
1102 let mut sigint = signal(SignalKind::interrupt()).unwrap();
1103 tokio::select! {
1104 _ = sigterm.recv() => println!(" [nedbd] SIGTERM — flushing and exiting..."),
1105 _ = sigint.recv() => println!(" [nedbd] SIGINT — flushing and exiting..."),
1106 }
1107 }
1108 #[cfg(not(unix))]
1109 {
1110 tokio::signal::ctrl_c().await.ok();
1111 println!(" [nedbd] shutting down — flushing manifests...");
1112 }
1113 };
1114
1115 axum::serve(listener, app)
1116 .tcp_nodelay(true)
1117 .with_graceful_shutdown(shutdown)
1118 .await?;
1119
1120 mgr_for_shutdown.flush_all().await;
1122 println!(" [nedbd] goodbye");
1123 Ok(())
1124}