@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/.github/workflows/release.yml +125 -0
- package/Cargo.lock +1169 -0
- package/Cargo.toml +25 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/__tests__/strata.test.js +217 -0
- package/build.rs +5 -0
- package/index.d.ts +536 -0
- package/index.js +236 -0
- package/jest.config.js +5 -0
- package/npm/darwin-arm64/README.md +3 -0
- package/npm/darwin-arm64/package.json +41 -0
- package/npm/darwin-x64/README.md +3 -0
- package/npm/darwin-x64/package.json +41 -0
- package/npm/linux-arm64-gnu/README.md +3 -0
- package/npm/linux-arm64-gnu/package.json +44 -0
- package/npm/linux-arm64-musl/README.md +3 -0
- package/npm/linux-arm64-musl/package.json +44 -0
- package/npm/linux-x64-gnu/README.md +3 -0
- package/npm/linux-x64-gnu/package.json +44 -0
- package/npm/linux-x64-musl/README.md +3 -0
- package/npm/linux-x64-musl/package.json +44 -0
- package/npm/win32-x64-msvc/README.md +3 -0
- package/npm/win32-x64-msvc/package.json +41 -0
- package/package.json +67 -0
- package/src/lib.rs +775 -0
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
|
+
}
|