@stratadb/core 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib.rs ADDED
@@ -0,0 +1,775 @@
1
+ //! Node.js bindings for StrataDB.
2
+ //!
3
+ //! This module exposes the StrataDB API to Node.js via NAPI-RS.
4
+
5
+ #![deny(clippy::all)]
6
+
7
+ use napi_derive::napi;
8
+ use std::collections::HashMap;
9
+
10
+ use stratadb::{
11
+ BatchVectorEntry, BranchExportResult, BranchImportResult, BundleValidateResult,
12
+ CollectionInfo, DistanceMetric, Error as StrataError, MergeStrategy,
13
+ Strata as RustStrata, Value, VersionedBranchInfo, VersionedValue,
14
+ };
15
+
16
+ /// Convert a JavaScript value to a stratadb Value.
17
+ fn js_to_value(val: serde_json::Value) -> Value {
18
+ match val {
19
+ serde_json::Value::Null => Value::Null,
20
+ serde_json::Value::Bool(b) => Value::Bool(b),
21
+ serde_json::Value::Number(n) => {
22
+ if let Some(i) = n.as_i64() {
23
+ Value::Int(i)
24
+ } else if let Some(f) = n.as_f64() {
25
+ Value::Float(f)
26
+ } else {
27
+ Value::Null
28
+ }
29
+ }
30
+ serde_json::Value::String(s) => Value::String(s),
31
+ serde_json::Value::Array(arr) => {
32
+ Value::Array(arr.into_iter().map(js_to_value).collect())
33
+ }
34
+ serde_json::Value::Object(map) => {
35
+ let mut obj = HashMap::new();
36
+ for (k, v) in map {
37
+ obj.insert(k, js_to_value(v));
38
+ }
39
+ Value::Object(obj)
40
+ }
41
+ }
42
+ }
43
+
44
+ /// Convert a stratadb Value to a serde_json Value.
45
+ fn value_to_js(val: Value) -> serde_json::Value {
46
+ match val {
47
+ Value::Null => serde_json::Value::Null,
48
+ Value::Bool(b) => serde_json::Value::Bool(b),
49
+ Value::Int(i) => serde_json::Value::Number(i.into()),
50
+ Value::Float(f) => {
51
+ serde_json::Number::from_f64(f)
52
+ .map(serde_json::Value::Number)
53
+ .unwrap_or(serde_json::Value::Null)
54
+ }
55
+ Value::String(s) => serde_json::Value::String(s),
56
+ Value::Bytes(b) => {
57
+ // Encode bytes as base64
58
+ serde_json::Value::String(base64_encode(&b))
59
+ }
60
+ Value::Array(arr) => {
61
+ serde_json::Value::Array(arr.into_iter().map(value_to_js).collect())
62
+ }
63
+ Value::Object(map) => {
64
+ let obj: serde_json::Map<String, serde_json::Value> = map
65
+ .into_iter()
66
+ .map(|(k, v)| (k, value_to_js(v)))
67
+ .collect();
68
+ serde_json::Value::Object(obj)
69
+ }
70
+ }
71
+ }
72
+
73
+ /// Simple base64 encoding for bytes.
74
+ fn base64_encode(data: &[u8]) -> String {
75
+ use std::io::Write;
76
+ let mut buf = Vec::new();
77
+ let mut encoder = base64_encoder(&mut buf);
78
+ encoder.write_all(data).unwrap();
79
+ drop(encoder);
80
+ String::from_utf8(buf).unwrap()
81
+ }
82
+
83
+ fn base64_encoder(writer: &mut Vec<u8>) -> impl std::io::Write + '_ {
84
+ struct Base64Writer<'a>(&'a mut Vec<u8>);
85
+ impl<'a> std::io::Write for Base64Writer<'a> {
86
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
87
+ const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
88
+ for chunk in buf.chunks(3) {
89
+ let b0 = chunk[0] as usize;
90
+ let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
91
+ let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
92
+ self.0.push(ALPHABET[b0 >> 2]);
93
+ self.0.push(ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)]);
94
+ if chunk.len() > 1 {
95
+ self.0.push(ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)]);
96
+ } else {
97
+ self.0.push(b'=');
98
+ }
99
+ if chunk.len() > 2 {
100
+ self.0.push(ALPHABET[b2 & 0x3f]);
101
+ } else {
102
+ self.0.push(b'=');
103
+ }
104
+ }
105
+ Ok(buf.len())
106
+ }
107
+ fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
108
+ }
109
+ Base64Writer(writer)
110
+ }
111
+
112
+ /// Convert a VersionedValue to a JSON object.
113
+ fn versioned_to_js(vv: VersionedValue) -> serde_json::Value {
114
+ serde_json::json!({
115
+ "value": value_to_js(vv.value),
116
+ "version": vv.version,
117
+ "timestamp": vv.timestamp,
118
+ })
119
+ }
120
+
121
+ /// Convert stratadb error to napi Error.
122
+ fn to_napi_err(e: StrataError) -> napi::Error {
123
+ napi::Error::from_reason(format!("{}", e))
124
+ }
125
+
126
+ /// StrataDB database handle.
127
+ ///
128
+ /// This is the main entry point for interacting with StrataDB from Node.js.
129
+ #[napi]
130
+ pub struct Strata {
131
+ inner: RustStrata,
132
+ }
133
+
134
+ #[napi]
135
+ impl Strata {
136
+ /// Open a database at the given path.
137
+ #[napi(factory)]
138
+ pub fn open(path: String) -> napi::Result<Self> {
139
+ let inner = RustStrata::open(&path).map_err(to_napi_err)?;
140
+ Ok(Self { inner })
141
+ }
142
+
143
+ /// Create an in-memory database (no persistence).
144
+ #[napi(factory)]
145
+ pub fn cache() -> napi::Result<Self> {
146
+ let inner = RustStrata::cache().map_err(to_napi_err)?;
147
+ Ok(Self { inner })
148
+ }
149
+
150
+ // =========================================================================
151
+ // KV Store
152
+ // =========================================================================
153
+
154
+ /// Store a key-value pair.
155
+ #[napi(js_name = "kvPut")]
156
+ pub fn kv_put(&self, key: String, value: serde_json::Value) -> napi::Result<u32> {
157
+ let v = js_to_value(value);
158
+ self.inner.kv_put(&key, v).map(|n| n as u32).map_err(to_napi_err)
159
+ }
160
+
161
+ /// Get a value by key.
162
+ #[napi(js_name = "kvGet")]
163
+ pub fn kv_get(&self, key: String) -> napi::Result<serde_json::Value> {
164
+ match self.inner.kv_get(&key).map_err(to_napi_err)? {
165
+ Some(v) => Ok(value_to_js(v)),
166
+ None => Ok(serde_json::Value::Null),
167
+ }
168
+ }
169
+
170
+ /// Delete a key.
171
+ #[napi(js_name = "kvDelete")]
172
+ pub fn kv_delete(&self, key: String) -> napi::Result<bool> {
173
+ self.inner.kv_delete(&key).map_err(to_napi_err)
174
+ }
175
+
176
+ /// List keys with optional prefix filter.
177
+ #[napi(js_name = "kvList")]
178
+ pub fn kv_list(&self, prefix: Option<String>) -> napi::Result<Vec<String>> {
179
+ self.inner.kv_list(prefix.as_deref()).map_err(to_napi_err)
180
+ }
181
+
182
+ /// Get version history for a key.
183
+ #[napi(js_name = "kvHistory")]
184
+ pub fn kv_history(&self, key: String) -> napi::Result<serde_json::Value> {
185
+ match self.inner.kv_getv(&key).map_err(to_napi_err)? {
186
+ Some(versions) => {
187
+ let arr: Vec<serde_json::Value> = versions
188
+ .into_iter()
189
+ .map(versioned_to_js)
190
+ .collect();
191
+ Ok(serde_json::Value::Array(arr))
192
+ }
193
+ None => Ok(serde_json::Value::Null),
194
+ }
195
+ }
196
+
197
+ // =========================================================================
198
+ // State Cell
199
+ // =========================================================================
200
+
201
+ /// Set a state cell value.
202
+ #[napi(js_name = "stateSet")]
203
+ pub fn state_set(&self, cell: String, value: serde_json::Value) -> napi::Result<u32> {
204
+ let v = js_to_value(value);
205
+ self.inner.state_set(&cell, v).map(|n| n as u32).map_err(to_napi_err)
206
+ }
207
+
208
+ /// Get a state cell value.
209
+ #[napi(js_name = "stateGet")]
210
+ pub fn state_get(&self, cell: String) -> napi::Result<serde_json::Value> {
211
+ match self.inner.state_get(&cell).map_err(to_napi_err)? {
212
+ Some(v) => Ok(value_to_js(v)),
213
+ None => Ok(serde_json::Value::Null),
214
+ }
215
+ }
216
+
217
+ /// Initialize a state cell if it doesn't exist.
218
+ #[napi(js_name = "stateInit")]
219
+ pub fn state_init(&self, cell: String, value: serde_json::Value) -> napi::Result<u32> {
220
+ let v = js_to_value(value);
221
+ self.inner.state_init(&cell, v).map(|n| n as u32).map_err(to_napi_err)
222
+ }
223
+
224
+ /// Compare-and-swap update based on version.
225
+ #[napi(js_name = "stateCas")]
226
+ pub fn state_cas(
227
+ &self,
228
+ cell: String,
229
+ new_value: serde_json::Value,
230
+ expected_version: Option<u32>,
231
+ ) -> napi::Result<Option<u32>> {
232
+ let v = js_to_value(new_value);
233
+ let exp = expected_version.map(|n| n as u64);
234
+ self.inner
235
+ .state_cas(&cell, exp, v)
236
+ .map(|opt| opt.map(|n| n as u32))
237
+ .map_err(to_napi_err)
238
+ }
239
+
240
+ /// Get version history for a state cell.
241
+ #[napi(js_name = "stateHistory")]
242
+ pub fn state_history(&self, cell: String) -> napi::Result<serde_json::Value> {
243
+ match self.inner.state_getv(&cell).map_err(to_napi_err)? {
244
+ Some(versions) => {
245
+ let arr: Vec<serde_json::Value> = versions
246
+ .into_iter()
247
+ .map(versioned_to_js)
248
+ .collect();
249
+ Ok(serde_json::Value::Array(arr))
250
+ }
251
+ None => Ok(serde_json::Value::Null),
252
+ }
253
+ }
254
+
255
+ // =========================================================================
256
+ // Event Log
257
+ // =========================================================================
258
+
259
+ /// Append an event to the log.
260
+ #[napi(js_name = "eventAppend")]
261
+ pub fn event_append(&self, event_type: String, payload: serde_json::Value) -> napi::Result<u32> {
262
+ let v = js_to_value(payload);
263
+ self.inner.event_append(&event_type, v).map(|n| n as u32).map_err(to_napi_err)
264
+ }
265
+
266
+ /// Get an event by sequence number.
267
+ #[napi(js_name = "eventGet")]
268
+ pub fn event_get(&self, sequence: u32) -> napi::Result<serde_json::Value> {
269
+ match self.inner.event_get(sequence as u64).map_err(to_napi_err)? {
270
+ Some(vv) => Ok(versioned_to_js(vv)),
271
+ None => Ok(serde_json::Value::Null),
272
+ }
273
+ }
274
+
275
+ /// List events by type.
276
+ #[napi(js_name = "eventList")]
277
+ pub fn event_list(&self, event_type: String) -> napi::Result<serde_json::Value> {
278
+ let events = self.inner.event_get_by_type(&event_type).map_err(to_napi_err)?;
279
+ let arr: Vec<serde_json::Value> = events.into_iter().map(versioned_to_js).collect();
280
+ Ok(serde_json::Value::Array(arr))
281
+ }
282
+
283
+ /// Get total event count.
284
+ #[napi(js_name = "eventLen")]
285
+ pub fn event_len(&self) -> napi::Result<u32> {
286
+ self.inner.event_len().map(|n| n as u32).map_err(to_napi_err)
287
+ }
288
+
289
+ // =========================================================================
290
+ // JSON Store
291
+ // =========================================================================
292
+
293
+ /// Set a value at a JSONPath.
294
+ #[napi(js_name = "jsonSet")]
295
+ pub fn json_set(&self, key: String, path: String, value: serde_json::Value) -> napi::Result<u32> {
296
+ let v = js_to_value(value);
297
+ self.inner.json_set(&key, &path, v).map(|n| n as u32).map_err(to_napi_err)
298
+ }
299
+
300
+ /// Get a value at a JSONPath.
301
+ #[napi(js_name = "jsonGet")]
302
+ pub fn json_get(&self, key: String, path: String) -> napi::Result<serde_json::Value> {
303
+ match self.inner.json_get(&key, &path).map_err(to_napi_err)? {
304
+ Some(v) => Ok(value_to_js(v)),
305
+ None => Ok(serde_json::Value::Null),
306
+ }
307
+ }
308
+
309
+ /// Delete a JSON document.
310
+ #[napi(js_name = "jsonDelete")]
311
+ pub fn json_delete(&self, key: String, path: String) -> napi::Result<u32> {
312
+ self.inner.json_delete(&key, &path).map(|n| n as u32).map_err(to_napi_err)
313
+ }
314
+
315
+ /// Get version history for a JSON document.
316
+ #[napi(js_name = "jsonHistory")]
317
+ pub fn json_history(&self, key: String) -> napi::Result<serde_json::Value> {
318
+ match self.inner.json_getv(&key).map_err(to_napi_err)? {
319
+ Some(versions) => {
320
+ let arr: Vec<serde_json::Value> = versions
321
+ .into_iter()
322
+ .map(versioned_to_js)
323
+ .collect();
324
+ Ok(serde_json::Value::Array(arr))
325
+ }
326
+ None => Ok(serde_json::Value::Null),
327
+ }
328
+ }
329
+
330
+ /// List JSON document keys.
331
+ #[napi(js_name = "jsonList")]
332
+ pub fn json_list(
333
+ &self,
334
+ limit: u32,
335
+ prefix: Option<String>,
336
+ cursor: Option<String>,
337
+ ) -> napi::Result<serde_json::Value> {
338
+ let (keys, next_cursor) = self
339
+ .inner
340
+ .json_list(prefix, cursor, limit as u64)
341
+ .map_err(to_napi_err)?;
342
+ Ok(serde_json::json!({
343
+ "keys": keys,
344
+ "cursor": next_cursor,
345
+ }))
346
+ }
347
+
348
+ // =========================================================================
349
+ // Vector Store
350
+ // =========================================================================
351
+
352
+ /// Create a vector collection.
353
+ #[napi(js_name = "vectorCreateCollection")]
354
+ pub fn vector_create_collection(
355
+ &self,
356
+ collection: String,
357
+ dimension: u32,
358
+ metric: Option<String>,
359
+ ) -> napi::Result<u32> {
360
+ let m = match metric.as_deref().unwrap_or("cosine") {
361
+ "cosine" => DistanceMetric::Cosine,
362
+ "euclidean" => DistanceMetric::Euclidean,
363
+ "dot_product" | "dotproduct" => DistanceMetric::DotProduct,
364
+ _ => return Err(napi::Error::from_reason("Invalid metric")),
365
+ };
366
+ self.inner
367
+ .vector_create_collection(&collection, dimension as u64, m)
368
+ .map(|n| n as u32)
369
+ .map_err(to_napi_err)
370
+ }
371
+
372
+ /// Delete a vector collection.
373
+ #[napi(js_name = "vectorDeleteCollection")]
374
+ pub fn vector_delete_collection(&self, collection: String) -> napi::Result<bool> {
375
+ self.inner
376
+ .vector_delete_collection(&collection)
377
+ .map_err(to_napi_err)
378
+ }
379
+
380
+ /// List vector collections.
381
+ #[napi(js_name = "vectorListCollections")]
382
+ pub fn vector_list_collections(&self) -> napi::Result<serde_json::Value> {
383
+ let collections = self.inner.vector_list_collections().map_err(to_napi_err)?;
384
+ let arr: Vec<serde_json::Value> = collections
385
+ .into_iter()
386
+ .map(collection_info_to_js)
387
+ .collect();
388
+ Ok(serde_json::Value::Array(arr))
389
+ }
390
+
391
+ /// Insert or update a vector.
392
+ #[napi(js_name = "vectorUpsert")]
393
+ pub fn vector_upsert(
394
+ &self,
395
+ collection: String,
396
+ key: String,
397
+ vector: Vec<f64>,
398
+ metadata: Option<serde_json::Value>,
399
+ ) -> napi::Result<u32> {
400
+ let vec: Vec<f32> = vector.iter().map(|&f| f as f32).collect();
401
+ let meta = metadata.map(js_to_value);
402
+ self.inner
403
+ .vector_upsert(&collection, &key, vec, meta)
404
+ .map(|n| n as u32)
405
+ .map_err(to_napi_err)
406
+ }
407
+
408
+ /// Get a vector by key.
409
+ #[napi(js_name = "vectorGet")]
410
+ pub fn vector_get(&self, collection: String, key: String) -> napi::Result<serde_json::Value> {
411
+ match self.inner.vector_get(&collection, &key).map_err(to_napi_err)? {
412
+ Some(vd) => {
413
+ let embedding: Vec<f64> = vd.data.embedding.iter().map(|&f| f as f64).collect();
414
+ Ok(serde_json::json!({
415
+ "key": vd.key,
416
+ "embedding": embedding,
417
+ "metadata": vd.data.metadata.map(value_to_js),
418
+ "version": vd.version,
419
+ "timestamp": vd.timestamp,
420
+ }))
421
+ }
422
+ None => Ok(serde_json::Value::Null),
423
+ }
424
+ }
425
+
426
+ /// Delete a vector.
427
+ #[napi(js_name = "vectorDelete")]
428
+ pub fn vector_delete(&self, collection: String, key: String) -> napi::Result<bool> {
429
+ self.inner.vector_delete(&collection, &key).map_err(to_napi_err)
430
+ }
431
+
432
+ /// Search for similar vectors.
433
+ #[napi(js_name = "vectorSearch")]
434
+ pub fn vector_search(
435
+ &self,
436
+ collection: String,
437
+ query: Vec<f64>,
438
+ k: u32,
439
+ ) -> napi::Result<serde_json::Value> {
440
+ let vec: Vec<f32> = query.iter().map(|&f| f as f32).collect();
441
+ let matches = self
442
+ .inner
443
+ .vector_search(&collection, vec, k as u64)
444
+ .map_err(to_napi_err)?;
445
+ let arr: Vec<serde_json::Value> = matches
446
+ .into_iter()
447
+ .map(|m| {
448
+ serde_json::json!({
449
+ "key": m.key,
450
+ "score": m.score,
451
+ "metadata": m.metadata.map(value_to_js),
452
+ })
453
+ })
454
+ .collect();
455
+ Ok(serde_json::Value::Array(arr))
456
+ }
457
+
458
+ /// Get statistics for a single collection.
459
+ #[napi(js_name = "vectorCollectionStats")]
460
+ pub fn vector_collection_stats(&self, collection: String) -> napi::Result<serde_json::Value> {
461
+ let info = self
462
+ .inner
463
+ .vector_collection_stats(&collection)
464
+ .map_err(to_napi_err)?;
465
+ Ok(collection_info_to_js(info))
466
+ }
467
+
468
+ /// Batch insert/update multiple vectors.
469
+ ///
470
+ /// Each vector should be an object with 'key', 'vector', and optional 'metadata'.
471
+ #[napi(js_name = "vectorBatchUpsert")]
472
+ pub fn vector_batch_upsert(
473
+ &self,
474
+ collection: String,
475
+ vectors: Vec<serde_json::Value>,
476
+ ) -> napi::Result<Vec<u32>> {
477
+ let batch: Vec<BatchVectorEntry> = vectors
478
+ .into_iter()
479
+ .map(|v| {
480
+ let obj = v
481
+ .as_object()
482
+ .ok_or_else(|| napi::Error::from_reason("Expected object"))?;
483
+ let key = obj
484
+ .get("key")
485
+ .and_then(|k| k.as_str())
486
+ .ok_or_else(|| napi::Error::from_reason("Missing 'key'"))?
487
+ .to_string();
488
+ let vec: Vec<f32> = obj
489
+ .get("vector")
490
+ .and_then(|v| v.as_array())
491
+ .ok_or_else(|| napi::Error::from_reason("Missing 'vector'"))?
492
+ .iter()
493
+ .map(|n| n.as_f64().unwrap_or(0.0) as f32)
494
+ .collect();
495
+ let meta = obj.get("metadata").map(|m| js_to_value(m.clone()));
496
+ Ok(BatchVectorEntry {
497
+ key,
498
+ vector: vec,
499
+ metadata: meta,
500
+ })
501
+ })
502
+ .collect::<napi::Result<_>>()?;
503
+ self.inner
504
+ .vector_batch_upsert(&collection, batch)
505
+ .map(|versions| versions.into_iter().map(|v| v as u32).collect())
506
+ .map_err(to_napi_err)
507
+ }
508
+
509
+ // =========================================================================
510
+ // Branch Management
511
+ // =========================================================================
512
+
513
+ /// Get the current branch name.
514
+ #[napi(js_name = "currentBranch")]
515
+ pub fn current_branch(&self) -> String {
516
+ self.inner.current_branch().to_string()
517
+ }
518
+
519
+ /// Switch to a different branch.
520
+ #[napi(js_name = "setBranch")]
521
+ pub fn set_branch(&mut self, branch: String) -> napi::Result<()> {
522
+ self.inner.set_branch(&branch).map_err(to_napi_err)
523
+ }
524
+
525
+ /// Create a new empty branch.
526
+ #[napi(js_name = "createBranch")]
527
+ pub fn create_branch(&self, branch: String) -> napi::Result<()> {
528
+ self.inner.create_branch(&branch).map_err(to_napi_err)
529
+ }
530
+
531
+ /// Fork the current branch to a new branch, copying all data.
532
+ #[napi(js_name = "forkBranch")]
533
+ pub fn fork_branch(&self, destination: String) -> napi::Result<serde_json::Value> {
534
+ let info = self.inner.fork_branch(&destination).map_err(to_napi_err)?;
535
+ Ok(serde_json::json!({
536
+ "source": info.source,
537
+ "destination": info.destination,
538
+ "keysCopied": info.keys_copied,
539
+ }))
540
+ }
541
+
542
+ /// List all branches.
543
+ #[napi(js_name = "listBranches")]
544
+ pub fn list_branches(&self) -> napi::Result<Vec<String>> {
545
+ self.inner.list_branches().map_err(to_napi_err)
546
+ }
547
+
548
+ /// Delete a branch.
549
+ #[napi(js_name = "deleteBranch")]
550
+ pub fn delete_branch(&self, branch: String) -> napi::Result<()> {
551
+ self.inner.delete_branch(&branch).map_err(to_napi_err)
552
+ }
553
+
554
+ /// Check if a branch exists.
555
+ #[napi(js_name = "branchExists")]
556
+ pub fn branch_exists(&self, name: String) -> napi::Result<bool> {
557
+ self.inner.branches().exists(&name).map_err(to_napi_err)
558
+ }
559
+
560
+ /// Get branch metadata with version info.
561
+ ///
562
+ /// Returns an object with branch info, or null if the branch does not exist.
563
+ #[napi(js_name = "branchGet")]
564
+ pub fn branch_get(&self, name: String) -> napi::Result<serde_json::Value> {
565
+ match self.inner.branch_get(&name).map_err(to_napi_err)? {
566
+ Some(info) => Ok(versioned_branch_info_to_js(info)),
567
+ None => Ok(serde_json::Value::Null),
568
+ }
569
+ }
570
+
571
+ /// Compare two branches.
572
+ #[napi(js_name = "diffBranches")]
573
+ pub fn diff_branches(&self, branch_a: String, branch_b: String) -> napi::Result<serde_json::Value> {
574
+ let diff = self
575
+ .inner
576
+ .diff_branches(&branch_a, &branch_b)
577
+ .map_err(to_napi_err)?;
578
+ Ok(serde_json::json!({
579
+ "branchA": diff.branch_a,
580
+ "branchB": diff.branch_b,
581
+ "summary": {
582
+ "totalAdded": diff.summary.total_added,
583
+ "totalRemoved": diff.summary.total_removed,
584
+ "totalModified": diff.summary.total_modified,
585
+ },
586
+ }))
587
+ }
588
+
589
+ /// Merge a branch into the current branch.
590
+ #[napi(js_name = "mergeBranches")]
591
+ pub fn merge_branches(
592
+ &self,
593
+ source: String,
594
+ strategy: Option<String>,
595
+ ) -> napi::Result<serde_json::Value> {
596
+ let strat = match strategy.as_deref().unwrap_or("last_writer_wins") {
597
+ "last_writer_wins" => MergeStrategy::LastWriterWins,
598
+ "strict" => MergeStrategy::Strict,
599
+ _ => return Err(napi::Error::from_reason("Invalid merge strategy")),
600
+ };
601
+ let target = self.inner.current_branch().to_string();
602
+ let info = self
603
+ .inner
604
+ .merge_branches(&source, &target, strat)
605
+ .map_err(to_napi_err)?;
606
+ let conflicts: Vec<serde_json::Value> = info
607
+ .conflicts
608
+ .into_iter()
609
+ .map(|c| {
610
+ serde_json::json!({
611
+ "key": c.key,
612
+ "space": c.space,
613
+ })
614
+ })
615
+ .collect();
616
+ Ok(serde_json::json!({
617
+ "keysApplied": info.keys_applied,
618
+ "spacesMerged": info.spaces_merged,
619
+ "conflicts": conflicts,
620
+ }))
621
+ }
622
+
623
+ // =========================================================================
624
+ // Space Management
625
+ // =========================================================================
626
+
627
+ /// Get the current space name.
628
+ #[napi(js_name = "currentSpace")]
629
+ pub fn current_space(&self) -> String {
630
+ self.inner.current_space().to_string()
631
+ }
632
+
633
+ /// Switch to a different space.
634
+ #[napi(js_name = "setSpace")]
635
+ pub fn set_space(&mut self, space: String) -> napi::Result<()> {
636
+ self.inner.set_space(&space).map_err(to_napi_err)
637
+ }
638
+
639
+ /// List all spaces in the current branch.
640
+ #[napi(js_name = "listSpaces")]
641
+ pub fn list_spaces(&self) -> napi::Result<Vec<String>> {
642
+ self.inner.list_spaces().map_err(to_napi_err)
643
+ }
644
+
645
+ /// Delete a space and all its data.
646
+ #[napi(js_name = "deleteSpace")]
647
+ pub fn delete_space(&self, space: String) -> napi::Result<()> {
648
+ self.inner.delete_space(&space).map_err(to_napi_err)
649
+ }
650
+
651
+ /// Force delete a space even if non-empty.
652
+ #[napi(js_name = "deleteSpaceForce")]
653
+ pub fn delete_space_force(&self, space: String) -> napi::Result<()> {
654
+ self.inner.delete_space_force(&space).map_err(to_napi_err)
655
+ }
656
+
657
+ // =========================================================================
658
+ // Database Operations
659
+ // =========================================================================
660
+
661
+ /// Check database connectivity.
662
+ #[napi]
663
+ pub fn ping(&self) -> napi::Result<String> {
664
+ self.inner.ping().map_err(to_napi_err)
665
+ }
666
+
667
+ /// Get database info.
668
+ #[napi]
669
+ pub fn info(&self) -> napi::Result<serde_json::Value> {
670
+ let info = self.inner.info().map_err(to_napi_err)?;
671
+ Ok(serde_json::json!({
672
+ "version": info.version,
673
+ "uptimeSecs": info.uptime_secs,
674
+ "branchCount": info.branch_count,
675
+ "totalKeys": info.total_keys,
676
+ }))
677
+ }
678
+
679
+ /// Flush writes to disk.
680
+ #[napi]
681
+ pub fn flush(&self) -> napi::Result<()> {
682
+ self.inner.flush().map_err(to_napi_err)
683
+ }
684
+
685
+ /// Trigger compaction.
686
+ #[napi]
687
+ pub fn compact(&self) -> napi::Result<()> {
688
+ self.inner.compact().map_err(to_napi_err)
689
+ }
690
+
691
+ // =========================================================================
692
+ // Bundle Operations
693
+ // =========================================================================
694
+
695
+ /// Export a branch to a bundle file.
696
+ #[napi(js_name = "branchExport")]
697
+ pub fn branch_export(&self, branch: String, path: String) -> napi::Result<serde_json::Value> {
698
+ let result = self
699
+ .inner
700
+ .branch_export(&branch, &path)
701
+ .map_err(to_napi_err)?;
702
+ Ok(branch_export_result_to_js(result))
703
+ }
704
+
705
+ /// Import a branch from a bundle file.
706
+ #[napi(js_name = "branchImport")]
707
+ pub fn branch_import(&self, path: String) -> napi::Result<serde_json::Value> {
708
+ let result = self.inner.branch_import(&path).map_err(to_napi_err)?;
709
+ Ok(branch_import_result_to_js(result))
710
+ }
711
+
712
+ /// Validate a bundle file without importing.
713
+ #[napi(js_name = "branchValidateBundle")]
714
+ pub fn branch_validate_bundle(&self, path: String) -> napi::Result<serde_json::Value> {
715
+ let result = self
716
+ .inner
717
+ .branch_validate_bundle(&path)
718
+ .map_err(to_napi_err)?;
719
+ Ok(bundle_validate_result_to_js(result))
720
+ }
721
+ }
722
+
723
+ /// Convert CollectionInfo to JSON.
724
+ fn collection_info_to_js(c: CollectionInfo) -> serde_json::Value {
725
+ serde_json::json!({
726
+ "name": c.name,
727
+ "dimension": c.dimension,
728
+ "metric": format!("{:?}", c.metric).to_lowercase(),
729
+ "count": c.count,
730
+ "indexType": c.index_type,
731
+ "memoryBytes": c.memory_bytes,
732
+ })
733
+ }
734
+
735
+ /// Convert VersionedBranchInfo to JSON.
736
+ fn versioned_branch_info_to_js(info: VersionedBranchInfo) -> serde_json::Value {
737
+ serde_json::json!({
738
+ "id": info.info.id.as_str(),
739
+ "status": format!("{:?}", info.info.status).to_lowercase(),
740
+ "createdAt": info.info.created_at,
741
+ "updatedAt": info.info.updated_at,
742
+ "parentId": info.info.parent_id.map(|p| p.as_str().to_string()),
743
+ "version": info.version,
744
+ "timestamp": info.timestamp,
745
+ })
746
+ }
747
+
748
+ /// Convert BranchExportResult to JSON.
749
+ fn branch_export_result_to_js(r: BranchExportResult) -> serde_json::Value {
750
+ serde_json::json!({
751
+ "branchId": r.branch_id,
752
+ "path": r.path,
753
+ "entryCount": r.entry_count,
754
+ "bundleSize": r.bundle_size,
755
+ })
756
+ }
757
+
758
+ /// Convert BranchImportResult to JSON.
759
+ fn branch_import_result_to_js(r: BranchImportResult) -> serde_json::Value {
760
+ serde_json::json!({
761
+ "branchId": r.branch_id,
762
+ "transactionsApplied": r.transactions_applied,
763
+ "keysWritten": r.keys_written,
764
+ })
765
+ }
766
+
767
+ /// Convert BundleValidateResult to JSON.
768
+ fn bundle_validate_result_to_js(r: BundleValidateResult) -> serde_json::Value {
769
+ serde_json::json!({
770
+ "branchId": r.branch_id,
771
+ "formatVersion": r.format_version,
772
+ "entryCount": r.entry_count,
773
+ "checksumsValid": r.checksums_valid,
774
+ })
775
+ }