tauri-plugin-velesdb 1.13.3

Tauri plugin for VelesDB - Vector search in desktop apps
Documentation
// Tauri plugin - pedantic/nursery lints relaxed
#![allow(clippy::pedantic)]
#![allow(clippy::nursery)]

//! # tauri-plugin-velesdb
//!
//! A Tauri plugin for `VelesDB` - Vector search in desktop applications.
//!
//! This plugin provides seamless integration of `VelesDB`'s vector database
//! capabilities into Tauri desktop applications.
//!
//! ## Features
//!
//! - **Collection Management**: Create, list, and delete vector collections
//! - **Vector Operations**: Insert, update, and delete vectors with payloads
//! - **Vector Search**: Fast similarity search with multiple distance metrics
//! - **Text Search**: BM25 full-text search across payloads
//! - **Hybrid Search**: Combined vector + text search with RRF fusion
//! - **`VelesQL`**: SQL-like query language for advanced searches
//!
//! ## Usage
//!
//! ### Rust (Plugin Registration)
//!
//! ```rust,ignore
//! fn main() {
//!     tauri::Builder::default()
//!         .plugin(tauri_plugin_velesdb::init("./data"))
//!         .run(tauri::generate_context!())
//!         .expect("error while running tauri application");
//! }
//! ```
//!
//! ### JavaScript (Frontend)
//!
//! ```javascript
//! import { invoke } from '@tauri-apps/api/core';
//!
//! // Create a collection
//! await invoke('plugin:velesdb|create_collection', {
//!   request: { name: 'documents', dimension: 768, metric: 'cosine' }
//! });
//!
//! // Insert vectors
//! await invoke('plugin:velesdb|upsert', {
//!   request: {
//!     collection: 'documents',
//!     points: [{ id: 1, vector: [...], payload: { title: 'Doc' } }]
//!   }
//! });
//!
//! // Search
//! const results = await invoke('plugin:velesdb|search', {
//!   request: { collection: 'documents', vector: [...], topK: 10 }
//! });
//! ```

#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]

use std::path::Path;

use tauri::{
    plugin::{Builder, TauriPlugin},
    Manager, Runtime,
};

pub mod commands;
pub mod commands_graph;
pub mod commands_index;
pub mod commands_memory;
pub mod commands_query;
pub mod commands_sparse;
pub mod error;
pub mod events;
pub mod helpers;
pub mod state;
pub mod types;
pub mod types_graph;

pub use error::{CommandError, Error, Result};
pub use state::VelesDbState;

/// Initializes the `VelesDB` plugin with the default settings.
///
/// Uses `./velesdb_data` as the default path for persistence.
/// This is the simplest way to get started.
///
/// # Example
///
/// ```rust,ignore
/// tauri::Builder::default()
///     .plugin(tauri_plugin_velesdb::init())
///     .run(tauri::generate_context!())
///     .expect("error while running tauri application");
/// ```
#[must_use]
pub fn init<R: Runtime>() -> TauriPlugin<R> {
    init_with_path("./velesdb_data")
}

/// Generates a Tauri invoke handler that registers common commands
/// unconditionally and persistence-only commands behind the
/// `persistence` feature flag.
///
/// Each command appears in exactly one list, eliminating the duplicated
/// `#[cfg(feature = "persistence")]` / `#[cfg(not(...))]` blocks.
macro_rules! velesdb_invoke_handler {
    (
        common: [$($common:path),* $(,)?],
        persistence_only: [$($persist:path),* $(,)?] $(,)?
    ) => {{
        #[cfg(feature = "persistence")]
        {
            tauri::generate_handler![$($common,)* $($persist,)*]
        }
        #[cfg(not(feature = "persistence"))]
        {
            tauri::generate_handler![$($common,)*]
        }
    }};
}

/// Initializes the `VelesDB` plugin with a custom data directory.
///
/// # Arguments
///
/// * `path` - Path to the database directory
///
/// # Example
///
/// ```rust,ignore
/// tauri::Builder::default()
///     .plugin(tauri_plugin_velesdb::init_with_path("./my_data"))
///     .run(tauri::generate_context!())
///     .expect("error while running tauri application");
/// ```
#[must_use]
pub fn init_with_path<R: Runtime, P: AsRef<Path>>(path: P) -> TauriPlugin<R> {
    let db_path = path.as_ref().to_path_buf();

    let builder = Builder::new("velesdb").invoke_handler(velesdb_invoke_handler!(
        common: [
            commands::create_collection,
            commands::create_metadata_collection,
            commands::delete_collection,
            commands::list_collections,
            commands::get_collection,
            commands::upsert,
            commands::upsert_metadata,
            commands::get_points,
            commands::delete_points,
            commands::search,
            commands::batch_search,
            commands::text_search,
            commands::hybrid_search,
            commands::multi_query_search,
            commands_query::query,
            commands::is_empty,
            commands::flush,
            commands::scroll_collection,
            // Sparse vector commands
            commands_sparse::sparse_search,
            commands_sparse::hybrid_sparse_search,
            commands_sparse::sparse_upsert,
            // PQ training command
            commands::train_pq,
            // AgentMemory commands (EPIC-016 US-003)
            commands_memory::semantic_store,
            commands_memory::semantic_query,
            commands_memory::episodic_record,
            commands_memory::episodic_recent,
            commands_memory::procedural_learn,
            commands_memory::procedural_recall,
            // Knowledge Graph commands (EPIC-015 US-001)
            commands_graph::create_graph_collection,
            commands_graph::add_edge,
            commands_graph::get_edges,
            commands_graph::traverse_graph,
            commands_graph::get_node_degree,
            commands_graph::traverse_graph_parallel,
            // Secondary Index commands
            commands_index::create_index,
            commands_index::drop_index,
            commands_index::list_indexes,
        ],
        persistence_only: [
            commands::stream_insert,
        ],
    ));
    builder
        .setup(move |app, _api| {
            let state = VelesDbState::new(db_path.clone());
            app.manage(state);
            tracing::info!("VelesDB plugin initialized with path: {:?}", db_path);
            Ok(())
        })
        .build()
}

/// Alias for `init()` for backward compatibility.
#[must_use]
pub fn init_default<R: Runtime>() -> TauriPlugin<R> {
    init()
}

/// Initializes the `VelesDB` plugin using the platform's app data directory.
///
/// This is the recommended way to initialize the plugin for production apps.
/// Data is stored in the standard location for each platform:
/// - **Windows**: `%APPDATA%\<app_name>\velesdb\`
/// - **macOS**: `~/Library/Application Support/<app_name>/velesdb/`
/// - **Linux**: `~/.local/share/<app_name>/velesdb/`
///
/// # Arguments
///
/// * `app_name` - Your application's name (used in the path)
///
/// # Errors
///
/// Returns an error if the platform's app data directory cannot be determined.
///
/// # Example
///
/// ```rust,ignore
/// tauri::Builder::default()
///     .plugin(tauri_plugin_velesdb::init_with_app_data("my-app")?)
///     .run(tauri::generate_context!())
///     .expect("error while running tauri application");
/// ```
pub fn init_with_app_data<R: Runtime>(app_name: &str) -> Result<TauriPlugin<R>> {
    let app_data_dir = get_app_data_dir(app_name)?;
    Ok(init_with_path(app_data_dir))
}

/// Returns the platform-specific app data directory for `VelesDB`.
///
/// # Arguments
///
/// * `app_name` - Your application's name
///
/// # Returns
///
/// Path to `<app_data>/<app_name>/velesdb/`, or an error if the platform
/// data directory cannot be determined.
///
/// # Errors
///
/// Returns `Error::InvalidConfig` if the platform data directory cannot be resolved.
pub fn get_app_data_dir(app_name: &str) -> Result<std::path::PathBuf> {
    let base_dir = dirs::data_dir().or_else(dirs::config_dir).ok_or_else(|| {
        Error::InvalidConfig("Could not determine app data directory for this platform".to_string())
    })?;

    Ok(base_dir.join(app_name).join("velesdb"))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_velesdb_state_creation() {
        // Arrange
        let path = std::path::PathBuf::from("/tmp/test");

        // Act
        let state = VelesDbState::new(path.clone());

        // Assert
        assert_eq!(state.path(), &path);
    }

    #[test]
    fn test_get_app_data_dir_structure() {
        // Act
        let path = get_app_data_dir("test-app").unwrap();

        // Assert - path should end with test-app/velesdb
        assert!(path.ends_with("test-app/velesdb") || path.ends_with("test-app\\velesdb"));
        assert!(path.to_string_lossy().contains("test-app"));
    }

    #[test]
    fn test_get_app_data_dir_different_apps() {
        // Act
        let path1 = get_app_data_dir("app1").unwrap();
        let path2 = get_app_data_dir("app2").unwrap();

        // Assert - different apps should have different paths
        assert_ne!(path1, path2);
    }
}