diff --git a/notes-api/Cargo.toml b/notes-api/Cargo.toml index 7891932..8a2be79 100644 --- a/notes-api/Cargo.toml +++ b/notes-api/Cargo.toml @@ -4,9 +4,25 @@ version = "0.1.0" edition = "2024" default-run = "notes-api" +[features] +default = ["sqlite", "smart-features"] +sqlite = [ + "notes-infra/sqlite", + "tower-sessions-sqlx-store/sqlite", + "sqlx/sqlite", +] +postgres = [ + "notes-infra/postgres", + "tower-sessions-sqlx-store/postgres", + "sqlx/postgres", +] +smart-features = ["notes-infra/smart-features", "dep:async-nats"] + [dependencies] notes-domain = { path = "../notes-domain" } -notes-infra = { path = "../notes-infra", features = ["sqlite"] } +notes-infra = { path = "../notes-infra", default-features = false, features = [ + "sqlite", +] } # Web framework axum = { version = "0.8.8", features = ["macros"] } @@ -20,7 +36,7 @@ tower-sessions-sqlx-store = { version = "0.15", features = ["sqlite"] } password-auth = "1.0" time = "0.3" async-trait = "0.1.89" -async-nats = "0.39" +async-nats = { version = "0.39", optional = true } # Async runtime tokio = { version = "1.48.0", features = ["full"] } diff --git a/notes-api/src/config.rs b/notes-api/src/config.rs index d01fe64..c20f5ee 100644 --- a/notes-api/src/config.rs +++ b/notes-api/src/config.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "smart-features")] use notes_infra::factory::{EmbeddingProvider, VectorProvider}; use std::env; @@ -10,7 +11,9 @@ pub struct Config { pub session_secret: String, pub cors_allowed_origins: Vec, pub allow_registration: bool, + #[cfg(feature = "smart-features")] pub embedding_provider: EmbeddingProvider, + #[cfg(feature = "smart-features")] pub vector_provider: VectorProvider, pub broker_url: String, } @@ -25,7 +28,9 @@ impl Default for Config { .to_string(), cors_allowed_origins: vec!["http://localhost:5173".to_string()], allow_registration: true, + #[cfg(feature = "smart-features")] embedding_provider: EmbeddingProvider::FastEmbed, + #[cfg(feature = "smart-features")] vector_provider: VectorProvider::Qdrant { url: "http://localhost:6334".to_string(), collection: "notes".to_string(), @@ -66,11 +71,13 @@ impl Config { .map(|s| s.to_lowercase() == "true") .unwrap_or(true); + #[cfg(feature = "smart-features")] let embedding_provider = match env::var("EMBEDDING_PROVIDER").unwrap_or_default().as_str() { // Future: "ollama" => EmbeddingProvider::Ollama(...), _ => EmbeddingProvider::FastEmbed, }; + #[cfg(feature = "smart-features")] let vector_provider = match env::var("VECTOR_PROVIDER").unwrap_or_default().as_str() { // Future: "postgres" => ... _ => VectorProvider::Qdrant { @@ -89,7 +96,9 @@ impl Config { session_secret, cors_allowed_origins, allow_registration, + #[cfg(feature = "smart-features")] embedding_provider, + #[cfg(feature = "smart-features")] vector_provider, broker_url, } diff --git a/notes-api/src/main.rs b/notes-api/src/main.rs index 3955ec3..6a50a97 100644 --- a/notes-api/src/main.rs +++ b/notes-api/src/main.rs @@ -43,9 +43,11 @@ async fn main() -> anyhow::Result<()> { tracing::info!("Connecting to database: {}", config.database_url); let db_config = DatabaseConfig::new(&config.database_url); + #[cfg(feature = "smart-features")] + use notes_infra::factory::build_link_repository; use notes_infra::factory::{ - build_database_pool, build_link_repository, build_note_repository, build_session_store, - build_tag_repository, build_user_repository, + build_database_pool, build_note_repository, build_session_store, build_tag_repository, + build_user_repository, }; let pool = build_database_pool(&db_config) .await @@ -73,6 +75,7 @@ async fn main() -> anyhow::Result<()> { let user_repo = build_user_repository(&pool) .await .map_err(|e| anyhow::anyhow!(e))?; + #[cfg(feature = "smart-features")] let link_repo = build_link_repository(&pool) .await .map_err(|e| anyhow::anyhow!(e))?; @@ -84,20 +87,26 @@ async fn main() -> anyhow::Result<()> { let user_service = Arc::new(UserService::new(user_repo.clone())); // Connect to NATS - tracing::info!("Connecting to NATS: {}", config.broker_url); - let nats_client = async_nats::connect(&config.broker_url) - .await - .map_err(|e| anyhow::anyhow!("NATS connection failed: {}", e))?; + // Connect to NATS + #[cfg(feature = "smart-features")] + let nats_client = { + tracing::info!("Connecting to NATS: {}", config.broker_url); + async_nats::connect(&config.broker_url) + .await + .map_err(|e| anyhow::anyhow!("NATS connection failed: {}", e))? + }; // Create application state let state = AppState::new( note_repo, tag_repo, user_repo.clone(), + #[cfg(feature = "smart-features")] link_repo, note_service, tag_service, user_service, + #[cfg(feature = "smart-features")] nats_client, config.clone(), ); diff --git a/notes-api/src/routes/mod.rs b/notes-api/src/routes/mod.rs index d15a9f0..01f11b3 100644 --- a/notes-api/src/routes/mod.rs +++ b/notes-api/src/routes/mod.rs @@ -15,7 +15,7 @@ use crate::state::AppState; /// Create the API v1 router pub fn api_v1_router() -> Router { - Router::new() + let router = Router::new() // Auth routes .route("/auth/register", post(auth::register)) .route("/auth/login", post(auth::login)) @@ -29,8 +29,12 @@ pub fn api_v1_router() -> Router { .patch(notes::update_note) .delete(notes::delete_note), ) - .route("/notes/{id}/versions", get(notes::list_note_versions)) - .route("/notes/{id}/related", get(notes::get_related_notes)) + .route("/notes/{id}/versions", get(notes::list_note_versions)); + + #[cfg(feature = "smart-features")] + let router = router.route("/notes/{id}/related", get(notes::get_related_notes)); + + router // Search route .route("/search", get(notes::search_notes)) // Import/Export routes diff --git a/notes-api/src/routes/notes.rs b/notes-api/src/routes/notes.rs index c8d37d8..ed6f238 100644 --- a/notes-api/src/routes/notes.rs +++ b/notes-api/src/routes/notes.rs @@ -83,15 +83,18 @@ pub async fn create_note( let note = state.note_service.create_note(domain_req).await?; // Publish event - let payload = serde_json::to_vec(¬e).unwrap_or_default(); - if let Err(e) = state - .nats_client - .publish("notes.updated", payload.into()) - .await + #[cfg(feature = "smart-features")] { - tracing::error!("Failed to publish notes.updated event: {}", e); - } else { - tracing::info!("Published notes.updated event for note {}", note.id); + let payload = serde_json::to_vec(¬e).unwrap_or_default(); + if let Err(e) = state + .nats_client + .publish("notes.updated", payload.into()) + .await + { + tracing::error!("Failed to publish notes.updated event: {}", e); + } else { + tracing::info!("Published notes.updated event for note {}", note.id); + } } Ok((StatusCode::CREATED, Json(NoteResponse::from(note)))) @@ -150,15 +153,18 @@ pub async fn update_note( let note = state.note_service.update_note(domain_req).await?; // Publish event - let payload = serde_json::to_vec(¬e).unwrap_or_default(); - if let Err(e) = state - .nats_client - .publish("notes.updated", payload.into()) - .await + #[cfg(feature = "smart-features")] { - tracing::error!("Failed to publish notes.updated event: {}", e); - } else { - tracing::info!("Published notes.updated event for note {}", note.id); + let payload = serde_json::to_vec(¬e).unwrap_or_default(); + if let Err(e) = state + .nats_client + .publish("notes.updated", payload.into()) + .await + { + tracing::error!("Failed to publish notes.updated event: {}", e); + } else { + tracing::info!("Published notes.updated event for note {}", note.id); + } } Ok(Json(NoteResponse::from(note))) @@ -228,6 +234,9 @@ pub async fn list_note_versions( /// Get related notes /// GET /api/v1/notes/:id/related +/// Get related notes +/// GET /api/v1/notes/:id/related +#[cfg(feature = "smart-features")] pub async fn get_related_notes( State(state): State, auth: AuthSession, diff --git a/notes-api/src/state.rs b/notes-api/src/state.rs index 9cd09f9..9a38ebc 100644 --- a/notes-api/src/state.rs +++ b/notes-api/src/state.rs @@ -11,10 +11,12 @@ pub struct AppState { pub note_repo: Arc, pub tag_repo: Arc, pub user_repo: Arc, + #[cfg(feature = "smart-features")] pub link_repo: Arc, pub note_service: Arc, pub tag_service: Arc, pub user_service: Arc, + #[cfg(feature = "smart-features")] pub nats_client: async_nats::Client, pub config: Config, } @@ -24,21 +26,23 @@ impl AppState { note_repo: Arc, tag_repo: Arc, user_repo: Arc, - link_repo: Arc, + #[cfg(feature = "smart-features")] link_repo: Arc, note_service: Arc, tag_service: Arc, user_service: Arc, - nats_client: async_nats::Client, + #[cfg(feature = "smart-features")] nats_client: async_nats::Client, config: Config, ) -> Self { Self { note_repo, tag_repo, user_repo, + #[cfg(feature = "smart-features")] link_repo, note_service, tag_service, user_service, + #[cfg(feature = "smart-features")] nats_client, config, } diff --git a/notes-infra/Cargo.toml b/notes-infra/Cargo.toml index 69bb067..cb3830e 100644 --- a/notes-infra/Cargo.toml +++ b/notes-infra/Cargo.toml @@ -4,9 +4,10 @@ version = "0.1.0" edition = "2024" [features] -default = ["sqlite"] +default = ["sqlite", "smart-features"] sqlite = ["sqlx/sqlite", "tower-sessions-sqlx-store/sqlite"] postgres = ["sqlx/postgres", "tower-sessions-sqlx-store/postgres"] +smart-features = ["dep:fastembed", "dep:qdrant-client"] [dependencies] notes-domain = { path = "../notes-domain" } @@ -19,6 +20,6 @@ tracing = "0.1" uuid = { version = "1.19.0", features = ["v4", "serde"] } tower-sessions = "0.14.0" tower-sessions-sqlx-store = { version = "0.15.0", default-features = false } -fastembed = "5.4" -qdrant-client = "1.16" +fastembed = { version = "5.4", optional = true } +qdrant-client = { version = "1.16", optional = true } serde_json = "1.0" diff --git a/notes-infra/src/factory.rs b/notes-infra/src/factory.rs index ab31777..94b79a8 100644 --- a/notes-infra/src/factory.rs +++ b/notes-infra/src/factory.rs @@ -17,6 +17,7 @@ pub enum FactoryError { pub type FactoryResult = Result; +#[cfg(feature = "smart-features")] #[derive(Debug, Clone)] pub enum EmbeddingProvider { FastEmbed, @@ -24,12 +25,14 @@ pub enum EmbeddingProvider { // OpenAI(String), // ApiKey } +#[cfg(feature = "smart-features")] #[derive(Debug, Clone)] pub enum VectorProvider { Qdrant { url: String, collection: String }, // InMemory, } +#[cfg(feature = "smart-features")] pub async fn build_embedding_generator( provider: &EmbeddingProvider, ) -> FactoryResult> { @@ -41,6 +44,7 @@ pub async fn build_embedding_generator( } } +#[cfg(feature = "smart-features")] pub async fn build_vector_store( provider: &VectorProvider, ) -> FactoryResult> { diff --git a/notes-infra/src/lib.rs b/notes-infra/src/lib.rs index 813b6fd..d8ae992 100644 --- a/notes-infra/src/lib.rs +++ b/notes-infra/src/lib.rs @@ -15,6 +15,7 @@ //! - [`db::run_migrations`] - Run database migrations pub mod db; +#[cfg(feature = "smart-features")] pub mod embeddings; pub mod factory; #[cfg(feature = "sqlite")] @@ -26,6 +27,7 @@ pub mod session_store; pub mod tag_repository; #[cfg(feature = "sqlite")] pub mod user_repository; +#[cfg(feature = "smart-features")] pub mod vector; // Re-export for convenience diff --git a/notes-worker/Cargo.toml b/notes-worker/Cargo.toml index fbf6e37..0a94082 100644 --- a/notes-worker/Cargo.toml +++ b/notes-worker/Cargo.toml @@ -3,11 +3,17 @@ name = "notes-worker" version = "0.1.0" edition = "2024" +[features] +default = ["sqlite", "smart-features"] +sqlite = ["notes-infra/sqlite", "sqlx/sqlite"] +# postgres = ["notes-infra/postgres", "sqlx/postgres"] +smart-features = ["notes-infra/smart-features"] + [dependencies] anyhow = "1.0.100" async-nats = "0.45.0" notes-domain = { path = "../notes-domain" } -notes-infra = { path = "../notes-infra" } +notes-infra = { path = "../notes-infra", default-features = false } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.146" tokio = { version = "1.48.0", features = ["full"] } diff --git a/notes-worker/src/config.rs b/notes-worker/src/config.rs index f942393..45cc439 100644 --- a/notes-worker/src/config.rs +++ b/notes-worker/src/config.rs @@ -1,10 +1,13 @@ +#[cfg(feature = "smart-features")] use notes_infra::factory::{EmbeddingProvider, VectorProvider}; #[derive(Debug, Clone)] pub struct Config { pub broker_url: String, pub database_url: String, + #[cfg(feature = "smart-features")] pub embedding_provider: EmbeddingProvider, + #[cfg(feature = "smart-features")] pub vector_provider: VectorProvider, } @@ -13,7 +16,9 @@ impl Default for Config { Self { broker_url: "nats://localhost:4222".to_string(), database_url: "sqlite::memory:".to_string(), + #[cfg(feature = "smart-features")] embedding_provider: EmbeddingProvider::FastEmbed, + #[cfg(feature = "smart-features")] vector_provider: VectorProvider::Qdrant { url: "http://localhost:6334".to_string(), collection: "notes".to_string(), @@ -26,6 +31,7 @@ impl Config { pub fn from_env() -> Self { let _ = dotenvy::dotenv(); + #[cfg(feature = "smart-features")] let embedding_provider = match std::env::var("EMBEDDING_PROVIDER") .unwrap_or_default() .as_str() @@ -33,6 +39,7 @@ impl Config { _ => EmbeddingProvider::FastEmbed, }; + #[cfg(feature = "smart-features")] let vector_provider = match std::env::var("VECTOR_PROVIDER") .unwrap_or_default() .as_str() @@ -48,7 +55,9 @@ impl Config { Self { broker_url: std::env::var("BROKER_URL").unwrap_or("nats://localhost:4222".to_string()), database_url: std::env::var("DATABASE_URL").unwrap_or("sqlite::memory:".to_string()), + #[cfg(feature = "smart-features")] embedding_provider, + #[cfg(feature = "smart-features")] vector_provider, } } diff --git a/notes-worker/src/main.rs b/notes-worker/src/main.rs index 3b9cabd..5a291a0 100644 --- a/notes-worker/src/main.rs +++ b/notes-worker/src/main.rs @@ -1,5 +1,7 @@ use futures_util::StreamExt; +#[cfg(feature = "smart-features")] use notes_domain::services::SmartNoteService; +#[cfg(feature = "smart-features")] use notes_infra::{ DatabaseConfig, factory::{ @@ -25,42 +27,51 @@ async fn main() -> anyhow::Result<()> { let config = Config::from_env(); let nats_client = async_nats::connect(&config.broker_url).await?; - let db_config = DatabaseConfig::new(config.database_url.clone()); - let db_pool = build_database_pool(&db_config).await?; - // Initialize smart feature adapters - let embedding_generator = build_embedding_generator(&config.embedding_provider).await?; - let vector_store = build_vector_store(&config.vector_provider).await?; - let link_repo = build_link_repository(&db_pool).await?; + #[cfg(feature = "smart-features")] + { + let db_config = DatabaseConfig::new(config.database_url.clone()); + let db_pool = build_database_pool(&db_config).await?; - // Create the service - let smart_service = SmartNoteService::new(embedding_generator, vector_store, link_repo); - tracing::info!( - "SmartNoteService initialized successfully with {:?}", - config.embedding_provider - ); + // Initialize smart feature adapters + let embedding_generator = build_embedding_generator(&config.embedding_provider).await?; + let vector_store = build_vector_store(&config.vector_provider).await?; + let link_repo = build_link_repository(&db_pool).await?; - // Subscribe to note update events - let mut subscriber = nats_client.subscribe("notes.updated").await?; - tracing::info!("Worker listening on 'notes.updated'..."); + // Create the service + let smart_service = SmartNoteService::new(embedding_generator, vector_store, link_repo); + tracing::info!( + "SmartNoteService initialized successfully with {:?}", + config.embedding_provider + ); - while let Some(msg) = subscriber.next().await { - // Parse message payload (assuming the payload IS the Note JSON) - let note_result: Result = serde_json::from_slice(&msg.payload); + // Subscribe to note update events + let mut subscriber = nats_client.subscribe("notes.updated").await?; + tracing::info!("Worker listening on 'notes.updated'..."); - match note_result { - Ok(note) => { - tracing::info!("Processing smart features for note: {}", note.id); - match smart_service.process_note(¬e).await { - Ok(_) => tracing::info!("Successfully processed note {}", note.id), - Err(e) => tracing::error!("Failed to process note {}: {}", note.id, e), + while let Some(msg) = subscriber.next().await { + // Parse message payload (assuming the payload IS the Note JSON) + let note_result: Result = serde_json::from_slice(&msg.payload); + + match note_result { + Ok(note) => { + tracing::info!("Processing smart features for note: {}", note.id); + match smart_service.process_note(¬e).await { + Ok(_) => tracing::info!("Successfully processed note {}", note.id), + Err(e) => tracing::error!("Failed to process note {}: {}", note.id, e), + } + } + Err(e) => { + tracing::error!("Failed to deserialize note from message: {}", e); } - } - Err(e) => { - tracing::error!("Failed to deserialize note from message: {}", e); } } } + #[cfg(not(feature = "smart-features"))] + { + tracing::info!("Smart features are disabled. Worker will exit."); + } + Ok(()) }