ruvector_node/
lib.rs

1//! Node.js bindings for Ruvector via NAPI-RS
2//!
3//! High-performance Rust vector database with zero-copy buffer sharing,
4//! async/await support, and complete TypeScript type definitions.
5
6#![deny(clippy::all)]
7#![warn(clippy::pedantic)]
8
9use napi::bindgen_prelude::*;
10use napi_derive::napi;
11use ruvector_core::{
12    types::{DbOptions, HnswConfig, QuantizationConfig},
13    DistanceMetric, SearchQuery, SearchResult, VectorDB as CoreVectorDB, VectorEntry,
14};
15use std::sync::Arc;
16use std::sync::RwLock;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19// Import new crates
20use ruvector_collections::CollectionManager as CoreCollectionManager;
21use ruvector_filter::FilterExpression;
22use ruvector_metrics::{gather_metrics, HealthChecker, HealthStatus};
23use std::path::PathBuf;
24
25/// Distance metric for similarity calculation
26#[napi(string_enum)]
27#[derive(Debug)]
28pub enum JsDistanceMetric {
29    /// Euclidean (L2) distance
30    Euclidean,
31    /// Cosine similarity (converted to distance)
32    Cosine,
33    /// Dot product (converted to distance for maximization)
34    DotProduct,
35    /// Manhattan (L1) distance
36    Manhattan,
37}
38
39impl From<JsDistanceMetric> for DistanceMetric {
40    fn from(metric: JsDistanceMetric) -> Self {
41        match metric {
42            JsDistanceMetric::Euclidean => DistanceMetric::Euclidean,
43            JsDistanceMetric::Cosine => DistanceMetric::Cosine,
44            JsDistanceMetric::DotProduct => DistanceMetric::DotProduct,
45            JsDistanceMetric::Manhattan => DistanceMetric::Manhattan,
46        }
47    }
48}
49
50/// Quantization configuration
51#[napi(object)]
52#[derive(Debug, Clone)]
53pub struct JsQuantizationConfig {
54    /// Quantization type: "none", "scalar", "product", "binary"
55    pub r#type: String,
56    /// Number of subspaces (for product quantization)
57    pub subspaces: Option<u32>,
58    /// Codebook size (for product quantization)
59    pub k: Option<u32>,
60}
61
62impl From<JsQuantizationConfig> for QuantizationConfig {
63    fn from(config: JsQuantizationConfig) -> Self {
64        match config.r#type.as_str() {
65            "none" => QuantizationConfig::None,
66            "scalar" => QuantizationConfig::Scalar,
67            "product" => QuantizationConfig::Product {
68                subspaces: config.subspaces.unwrap_or(16) as usize,
69                k: config.k.unwrap_or(256) as usize,
70            },
71            "binary" => QuantizationConfig::Binary,
72            _ => QuantizationConfig::Scalar,
73        }
74    }
75}
76
77/// HNSW index configuration
78#[napi(object)]
79#[derive(Debug, Clone)]
80pub struct JsHnswConfig {
81    /// Number of connections per layer (M)
82    pub m: Option<u32>,
83    /// Size of dynamic candidate list during construction
84    pub ef_construction: Option<u32>,
85    /// Size of dynamic candidate list during search
86    pub ef_search: Option<u32>,
87    /// Maximum number of elements
88    pub max_elements: Option<u32>,
89}
90
91impl From<JsHnswConfig> for HnswConfig {
92    fn from(config: JsHnswConfig) -> Self {
93        HnswConfig {
94            m: config.m.unwrap_or(32) as usize,
95            ef_construction: config.ef_construction.unwrap_or(200) as usize,
96            ef_search: config.ef_search.unwrap_or(100) as usize,
97            max_elements: config.max_elements.unwrap_or(10_000_000) as usize,
98        }
99    }
100}
101
102/// Database configuration options
103#[napi(object)]
104#[derive(Debug)]
105pub struct JsDbOptions {
106    /// Vector dimensions
107    pub dimensions: u32,
108    /// Distance metric
109    pub distance_metric: Option<JsDistanceMetric>,
110    /// Storage path
111    pub storage_path: Option<String>,
112    /// HNSW configuration
113    pub hnsw_config: Option<JsHnswConfig>,
114    /// Quantization configuration
115    pub quantization: Option<JsQuantizationConfig>,
116}
117
118impl From<JsDbOptions> for DbOptions {
119    fn from(options: JsDbOptions) -> Self {
120        DbOptions {
121            dimensions: options.dimensions as usize,
122            distance_metric: options
123                .distance_metric
124                .map(Into::into)
125                .unwrap_or(DistanceMetric::Cosine),
126            storage_path: options
127                .storage_path
128                .unwrap_or_else(|| "./ruvector.db".to_string()),
129            hnsw_config: options.hnsw_config.map(Into::into),
130            quantization: options.quantization.map(Into::into),
131        }
132    }
133}
134
135/// Vector entry
136#[napi(object)]
137pub struct JsVectorEntry {
138    /// Optional ID (auto-generated if not provided)
139    pub id: Option<String>,
140    /// Vector data as Float32Array or array of numbers
141    pub vector: Float32Array,
142    /// Optional metadata as JSON string (use JSON.stringify on objects)
143    pub metadata: Option<String>,
144}
145
146impl JsVectorEntry {
147    fn to_core(&self) -> Result<VectorEntry> {
148        // Parse JSON string to HashMap<String, serde_json::Value>
149        let metadata = self.metadata.as_ref().and_then(|s| {
150            serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(s).ok()
151        });
152
153        Ok(VectorEntry {
154            id: self.id.clone(),
155            vector: self.vector.to_vec(),
156            metadata,
157        })
158    }
159}
160
161/// Search query parameters
162#[napi(object)]
163pub struct JsSearchQuery {
164    /// Query vector as Float32Array or array of numbers
165    pub vector: Float32Array,
166    /// Number of results to return (top-k)
167    pub k: u32,
168    /// Optional ef_search parameter for HNSW
169    pub ef_search: Option<u32>,
170    /// Optional metadata filter as JSON string (use JSON.stringify on objects)
171    pub filter: Option<String>,
172}
173
174impl JsSearchQuery {
175    fn to_core(&self) -> Result<SearchQuery> {
176        // Parse JSON string to HashMap<String, serde_json::Value>
177        let filter = self.filter.as_ref().and_then(|s| {
178            serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(s).ok()
179        });
180
181        Ok(SearchQuery {
182            vector: self.vector.to_vec(),
183            k: self.k as usize,
184            filter,
185            ef_search: self.ef_search.map(|v| v as usize),
186        })
187    }
188}
189
190/// Search result with similarity score
191#[napi(object)]
192#[derive(Clone)]
193pub struct JsSearchResult {
194    /// Vector ID
195    pub id: String,
196    /// Distance/similarity score (lower is better for distance metrics)
197    pub score: f64,
198    /// Vector data (if requested)
199    pub vector: Option<Float32Array>,
200    /// Metadata as JSON string (use JSON.parse to convert to object)
201    pub metadata: Option<String>,
202}
203
204impl From<SearchResult> for JsSearchResult {
205    fn from(result: SearchResult) -> Self {
206        // Convert Vec<f32> to Float32Array
207        let vector = result.vector.map(|v| Float32Array::new(v));
208
209        // Convert HashMap to JSON string
210        let metadata = result.metadata.and_then(|m| serde_json::to_string(&m).ok());
211
212        JsSearchResult {
213            id: result.id,
214            score: f64::from(result.score),
215            vector,
216            metadata,
217        }
218    }
219}
220
221/// High-performance vector database with HNSW indexing
222#[napi]
223pub struct VectorDB {
224    inner: Arc<RwLock<CoreVectorDB>>,
225}
226
227#[napi]
228impl VectorDB {
229    /// Create a new vector database with the given options
230    ///
231    /// # Example
232    /// ```javascript
233    /// const db = new VectorDB({
234    ///   dimensions: 384,
235    ///   distanceMetric: 'Cosine',
236    ///   storagePath: './vectors.db',
237    ///   hnswConfig: {
238    ///     m: 32,
239    ///     efConstruction: 200,
240    ///     efSearch: 100
241    ///   }
242    /// });
243    /// ```
244    #[napi(constructor)]
245    pub fn new(options: JsDbOptions) -> Result<Self> {
246        let core_options: DbOptions = options.into();
247        let db = CoreVectorDB::new(core_options)
248            .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
249
250        Ok(Self {
251            inner: Arc::new(RwLock::new(db)),
252        })
253    }
254
255    /// Create a vector database with default options
256    ///
257    /// # Example
258    /// ```javascript
259    /// const db = VectorDB.withDimensions(384);
260    /// ```
261    #[napi(factory)]
262    pub fn with_dimensions(dimensions: u32) -> Result<Self> {
263        let db = CoreVectorDB::with_dimensions(dimensions as usize)
264            .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
265
266        Ok(Self {
267            inner: Arc::new(RwLock::new(db)),
268        })
269    }
270
271    /// Insert a vector entry into the database
272    ///
273    /// Returns the ID of the inserted vector (auto-generated if not provided)
274    ///
275    /// # Example
276    /// ```javascript
277    /// const id = await db.insert({
278    ///   vector: new Float32Array([1.0, 2.0, 3.0]),
279    ///   metadata: { text: 'example' }
280    /// });
281    /// ```
282    #[napi]
283    pub async fn insert(&self, entry: JsVectorEntry) -> Result<String> {
284        let core_entry = entry.to_core()?;
285        let db = self.inner.clone();
286
287        tokio::task::spawn_blocking(move || {
288            let db = db.read().expect("RwLock poisoned");
289            db.insert(core_entry)
290        })
291        .await
292        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
293        .map_err(|e| Error::from_reason(format!("Insert failed: {}", e)))
294    }
295
296    /// Insert multiple vectors in a batch
297    ///
298    /// Returns an array of IDs for the inserted vectors
299    ///
300    /// # Example
301    /// ```javascript
302    /// const ids = await db.insertBatch([
303    ///   { vector: new Float32Array([1, 2, 3]) },
304    ///   { vector: new Float32Array([4, 5, 6]) }
305    /// ]);
306    /// ```
307    #[napi]
308    pub async fn insert_batch(&self, entries: Vec<JsVectorEntry>) -> Result<Vec<String>> {
309        let core_entries: Result<Vec<VectorEntry>> = entries.iter().map(|e| e.to_core()).collect();
310        let core_entries = core_entries?;
311        let db = self.inner.clone();
312
313        tokio::task::spawn_blocking(move || {
314            let db = db.read().expect("RwLock poisoned");
315            db.insert_batch(core_entries)
316        })
317        .await
318        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
319        .map_err(|e| Error::from_reason(format!("Batch insert failed: {}", e)))
320    }
321
322    /// Search for similar vectors
323    ///
324    /// Returns an array of search results sorted by similarity
325    ///
326    /// # Example
327    /// ```javascript
328    /// const results = await db.search({
329    ///   vector: new Float32Array([1, 2, 3]),
330    ///   k: 10,
331    ///   filter: { category: 'example' }
332    /// });
333    /// ```
334    #[napi]
335    pub async fn search(&self, query: JsSearchQuery) -> Result<Vec<JsSearchResult>> {
336        let core_query = query.to_core()?;
337        let db = self.inner.clone();
338
339        tokio::task::spawn_blocking(move || {
340            let db = db.read().expect("RwLock poisoned");
341            db.search(core_query)
342        })
343        .await
344        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
345        .map_err(|e| Error::from_reason(format!("Search failed: {}", e)))
346        .map(|results| results.into_iter().map(Into::into).collect())
347    }
348
349    /// Delete a vector by ID
350    ///
351    /// Returns true if the vector was deleted, false if not found
352    ///
353    /// # Example
354    /// ```javascript
355    /// const deleted = await db.delete('vector-id');
356    /// ```
357    #[napi]
358    pub async fn delete(&self, id: String) -> Result<bool> {
359        let db = self.inner.clone();
360
361        tokio::task::spawn_blocking(move || {
362            let db = db.read().expect("RwLock poisoned");
363            db.delete(&id)
364        })
365        .await
366        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
367        .map_err(|e| Error::from_reason(format!("Delete failed: {}", e)))
368    }
369
370    /// Get a vector by ID
371    ///
372    /// Returns the vector entry if found, null otherwise
373    ///
374    /// # Example
375    /// ```javascript
376    /// const entry = await db.get('vector-id');
377    /// if (entry) {
378    ///   console.log('Found:', entry.metadata);
379    /// }
380    /// ```
381    #[napi]
382    pub async fn get(&self, id: String) -> Result<Option<JsVectorEntry>> {
383        let db = self.inner.clone();
384
385        let result = tokio::task::spawn_blocking(move || {
386            let db = db.read().expect("RwLock poisoned");
387            db.get(&id)
388        })
389        .await
390        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
391        .map_err(|e| Error::from_reason(format!("Get failed: {}", e)))?;
392
393        Ok(result.map(|entry| {
394            // Convert HashMap to JSON string
395            let metadata = entry.metadata.and_then(|m| serde_json::to_string(&m).ok());
396
397            JsVectorEntry {
398                id: entry.id,
399                vector: Float32Array::new(entry.vector),
400                metadata,
401            }
402        }))
403    }
404
405    /// Get the number of vectors in the database
406    ///
407    /// # Example
408    /// ```javascript
409    /// const count = await db.len();
410    /// console.log(`Database contains ${count} vectors`);
411    /// ```
412    #[napi]
413    pub async fn len(&self) -> Result<u32> {
414        let db = self.inner.clone();
415
416        tokio::task::spawn_blocking(move || {
417            let db = db.read().expect("RwLock poisoned");
418            db.len()
419        })
420        .await
421        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
422        .map_err(|e| Error::from_reason(format!("Len failed: {}", e)))
423        .map(|len| len as u32)
424    }
425
426    /// Check if the database is empty
427    ///
428    /// # Example
429    /// ```javascript
430    /// if (await db.isEmpty()) {
431    ///   console.log('Database is empty');
432    /// }
433    /// ```
434    #[napi]
435    pub async fn is_empty(&self) -> Result<bool> {
436        let db = self.inner.clone();
437
438        tokio::task::spawn_blocking(move || {
439            let db = db.read().expect("RwLock poisoned");
440            db.is_empty()
441        })
442        .await
443        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
444        .map_err(|e| Error::from_reason(format!("IsEmpty failed: {}", e)))
445    }
446}
447
448/// Get the version of the Ruvector library
449#[napi]
450pub fn version() -> String {
451    env!("CARGO_PKG_VERSION").to_string()
452}
453
454/// Test function to verify the bindings are working
455#[napi]
456pub fn hello() -> String {
457    "Hello from Ruvector Node.js bindings!".to_string()
458}
459
460/// Filter for metadata-based search
461#[napi(object)]
462#[derive(Debug, Clone)]
463pub struct JsFilter {
464    /// Field name to filter on
465    pub field: String,
466    /// Operator: "eq", "ne", "gt", "gte", "lt", "lte", "in", "match"
467    pub operator: String,
468    /// Value to compare against (JSON string)
469    pub value: String,
470}
471
472impl JsFilter {
473    fn to_filter_expression(&self) -> Result<FilterExpression> {
474        let value: serde_json::Value = serde_json::from_str(&self.value)
475            .map_err(|e| Error::from_reason(format!("Invalid JSON value: {}", e)))?;
476
477        Ok(match self.operator.as_str() {
478            "eq" => FilterExpression::eq(&self.field, value),
479            "ne" => FilterExpression::ne(&self.field, value),
480            "gt" => FilterExpression::gt(&self.field, value),
481            "gte" => FilterExpression::gte(&self.field, value),
482            "lt" => FilterExpression::lt(&self.field, value),
483            "lte" => FilterExpression::lte(&self.field, value),
484            "match" => FilterExpression::Match {
485                field: self.field.clone(),
486                text: value.as_str().unwrap_or("").to_string(),
487            },
488            _ => FilterExpression::eq(&self.field, value),
489        })
490    }
491}
492
493/// Collection configuration
494#[napi(object)]
495#[derive(Debug, Clone)]
496pub struct JsCollectionConfig {
497    /// Vector dimensions
498    pub dimensions: u32,
499    /// Distance metric
500    pub distance_metric: Option<JsDistanceMetric>,
501    /// HNSW configuration
502    pub hnsw_config: Option<JsHnswConfig>,
503    /// Quantization configuration
504    pub quantization: Option<JsQuantizationConfig>,
505}
506
507impl From<JsCollectionConfig> for ruvector_collections::CollectionConfig {
508    fn from(config: JsCollectionConfig) -> Self {
509        ruvector_collections::CollectionConfig {
510            dimensions: config.dimensions as usize,
511            distance_metric: config
512                .distance_metric
513                .map(Into::into)
514                .unwrap_or(DistanceMetric::Cosine),
515            hnsw_config: config.hnsw_config.map(Into::into),
516            quantization: config.quantization.map(Into::into),
517            on_disk_payload: true,
518        }
519    }
520}
521
522/// Collection statistics
523#[napi(object)]
524#[derive(Debug, Clone)]
525pub struct JsCollectionStats {
526    /// Number of vectors in the collection
527    pub vectors_count: u32,
528    /// Disk space used in bytes
529    pub disk_size_bytes: i64,
530    /// RAM space used in bytes
531    pub ram_size_bytes: i64,
532}
533
534impl From<ruvector_collections::CollectionStats> for JsCollectionStats {
535    fn from(stats: ruvector_collections::CollectionStats) -> Self {
536        JsCollectionStats {
537            vectors_count: stats.vectors_count as u32,
538            disk_size_bytes: stats.disk_size_bytes as i64,
539            ram_size_bytes: stats.ram_size_bytes as i64,
540        }
541    }
542}
543
544/// Collection alias
545#[napi(object)]
546#[derive(Debug, Clone)]
547pub struct JsAlias {
548    /// Alias name
549    pub alias: String,
550    /// Collection name
551    pub collection: String,
552}
553
554impl From<(String, String)> for JsAlias {
555    fn from(tuple: (String, String)) -> Self {
556        JsAlias {
557            alias: tuple.0,
558            collection: tuple.1,
559        }
560    }
561}
562
563/// Collection manager for multi-collection support
564#[napi]
565pub struct CollectionManager {
566    inner: Arc<RwLock<CoreCollectionManager>>,
567}
568
569#[napi]
570impl CollectionManager {
571    /// Create a new collection manager
572    ///
573    /// # Example
574    /// ```javascript
575    /// const manager = new CollectionManager('./collections');
576    /// ```
577    #[napi(constructor)]
578    pub fn new(base_path: Option<String>) -> Result<Self> {
579        let path = PathBuf::from(base_path.unwrap_or_else(|| "./collections".to_string()));
580        let manager = CoreCollectionManager::new(path).map_err(|e| {
581            Error::from_reason(format!("Failed to create collection manager: {}", e))
582        })?;
583
584        Ok(Self {
585            inner: Arc::new(RwLock::new(manager)),
586        })
587    }
588
589    /// Create a new collection
590    ///
591    /// # Example
592    /// ```javascript
593    /// await manager.createCollection('my_vectors', {
594    ///   dimensions: 384,
595    ///   distanceMetric: 'Cosine'
596    /// });
597    /// ```
598    #[napi]
599    pub async fn create_collection(&self, name: String, config: JsCollectionConfig) -> Result<()> {
600        let core_config: ruvector_collections::CollectionConfig = config.into();
601        let manager = self.inner.clone();
602
603        tokio::task::spawn_blocking(move || {
604            let manager = manager.write().expect("RwLock poisoned");
605            manager.create_collection(&name, core_config)
606        })
607        .await
608        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
609        .map_err(|e| Error::from_reason(format!("Failed to create collection: {}", e)))
610    }
611
612    /// List all collections
613    ///
614    /// # Example
615    /// ```javascript
616    /// const collections = await manager.listCollections();
617    /// console.log('Collections:', collections);
618    /// ```
619    #[napi]
620    pub async fn list_collections(&self) -> Result<Vec<String>> {
621        let manager = self.inner.clone();
622
623        tokio::task::spawn_blocking(move || {
624            let manager = manager.read().expect("RwLock poisoned");
625            manager.list_collections()
626        })
627        .await
628        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))
629    }
630
631    /// Delete a collection
632    ///
633    /// # Example
634    /// ```javascript
635    /// await manager.deleteCollection('my_vectors');
636    /// ```
637    #[napi]
638    pub async fn delete_collection(&self, name: String) -> Result<()> {
639        let manager = self.inner.clone();
640
641        tokio::task::spawn_blocking(move || {
642            let manager = manager.write().expect("RwLock poisoned");
643            manager.delete_collection(&name)
644        })
645        .await
646        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
647        .map_err(|e| Error::from_reason(format!("Failed to delete collection: {}", e)))
648    }
649
650    /// Get collection statistics
651    ///
652    /// # Example
653    /// ```javascript
654    /// const stats = await manager.getStats('my_vectors');
655    /// console.log(`Vectors: ${stats.vectorsCount}`);
656    /// ```
657    #[napi]
658    pub async fn get_stats(&self, name: String) -> Result<JsCollectionStats> {
659        let manager = self.inner.clone();
660
661        tokio::task::spawn_blocking(move || {
662            let manager = manager.read().expect("RwLock poisoned");
663            manager.collection_stats(&name)
664        })
665        .await
666        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
667        .map_err(|e| Error::from_reason(format!("Failed to get stats: {}", e)))
668        .map(Into::into)
669    }
670
671    /// Create an alias for a collection
672    ///
673    /// # Example
674    /// ```javascript
675    /// await manager.createAlias('latest', 'my_vectors_v2');
676    /// ```
677    #[napi]
678    pub async fn create_alias(&self, alias: String, collection: String) -> Result<()> {
679        let manager = self.inner.clone();
680
681        tokio::task::spawn_blocking(move || {
682            let manager = manager.write().expect("RwLock poisoned");
683            manager.create_alias(&alias, &collection)
684        })
685        .await
686        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
687        .map_err(|e| Error::from_reason(format!("Failed to create alias: {}", e)))
688    }
689
690    /// Delete an alias
691    ///
692    /// # Example
693    /// ```javascript
694    /// await manager.deleteAlias('latest');
695    /// ```
696    #[napi]
697    pub async fn delete_alias(&self, alias: String) -> Result<()> {
698        let manager = self.inner.clone();
699
700        tokio::task::spawn_blocking(move || {
701            let manager = manager.write().expect("RwLock poisoned");
702            manager.delete_alias(&alias)
703        })
704        .await
705        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
706        .map_err(|e| Error::from_reason(format!("Failed to delete alias: {}", e)))
707    }
708
709    /// List all aliases
710    ///
711    /// # Example
712    /// ```javascript
713    /// const aliases = await manager.listAliases();
714    /// for (const alias of aliases) {
715    ///   console.log(`${alias.alias} -> ${alias.collection}`);
716    /// }
717    /// ```
718    #[napi]
719    pub async fn list_aliases(&self) -> Result<Vec<JsAlias>> {
720        let manager = self.inner.clone();
721
722        let aliases = tokio::task::spawn_blocking(move || {
723            let manager = manager.read().expect("RwLock poisoned");
724            manager.list_aliases()
725        })
726        .await
727        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?;
728
729        Ok(aliases.into_iter().map(Into::into).collect())
730    }
731}
732
733/// Health response
734#[napi(object)]
735#[derive(Debug, Clone)]
736pub struct JsHealthResponse {
737    /// Status: "healthy", "degraded", or "unhealthy"
738    pub status: String,
739    /// Version string
740    pub version: String,
741    /// Uptime in seconds
742    pub uptime_seconds: i64,
743}
744
745/// Get Prometheus metrics
746///
747/// # Example
748/// ```javascript
749/// const metrics = getMetrics();
750/// console.log(metrics);
751/// ```
752#[napi]
753pub fn get_metrics() -> String {
754    gather_metrics()
755}
756
757/// Get health status
758///
759/// # Example
760/// ```javascript
761/// const health = getHealth();
762/// console.log(`Status: ${health.status}`);
763/// console.log(`Uptime: ${health.uptimeSeconds}s`);
764/// ```
765#[napi]
766pub fn get_health() -> JsHealthResponse {
767    let checker = HealthChecker::new();
768    let health = checker.health();
769
770    JsHealthResponse {
771        status: match health.status {
772            HealthStatus::Healthy => "healthy".to_string(),
773            HealthStatus::Degraded => "degraded".to_string(),
774            HealthStatus::Unhealthy => "unhealthy".to_string(),
775        },
776        version: health.version,
777        uptime_seconds: health.uptime_seconds as i64,
778    }
779}