#![allow(clippy::uninlined_format_args)] #![allow(clippy::manual_let_else)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_sign_loss)] #![allow(clippy::cast_precision_loss)] #![allow(clippy::ref_option)] #![allow(clippy::match_same_arms)] #![allow(clippy::trivially_copy_pass_by_ref)] #![allow(clippy::map_unwrap_or)] #![allow(clippy::enum_glob_use)] #![allow(clippy::unused_async)] #![allow(clippy::needless_for_each)] #![allow(clippy::doc_markdown)] #![allow(clippy::missing_errors_doc)] #![allow(clippy::must_use_candidate)] #![allow(clippy::similar_names)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::redundant_closure_for_method_calls)] #![allow(clippy::single_match_else)] #![allow(clippy::assigning_clones)]
pub mod auth;
pub mod config;
mod handlers;
pub mod onboarding;
pub mod rate_limit;
pub mod routes;
mod security_addon;
pub mod tls;
mod types;
use security_addon::SecurityAddon;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use utoipa::OpenApi;
use velesdb_core::guardrails::QueryLimits;
use velesdb_core::metrics::{DurationHistogram, OperationalMetrics, TraversalMetrics};
use velesdb_core::Database;
pub use onboarding::OnboardingMetrics;
pub use types::*;
pub use handlers::{
aggregate, analyze_collection, batch_search, bulk_delete_points, collection_sanity,
compact_collection, create_collection, create_index, delete_collection, delete_index,
delete_point, explain, flush_collection, get_collection, get_collection_config,
get_collection_stats, get_guardrails, get_point, health_check, hybrid_search, is_empty,
list_collections, list_indexes, match_query, multi_query_search, query, readiness_check,
rebuild_index, scroll_points, search, search_ids, stream_insert, stream_upsert_points,
text_search, update_guardrails, upsert_points, vacuum_collection,
};
pub use handlers::graph::{
add_edge, get_edge_count, get_edges, get_node_degree, get_node_edges, get_node_payload,
graph_search, list_nodes, remove_edge, stream_traverse, traverse_graph, traverse_parallel,
upsert_node_payload, DegreeResponse, EdgeCountResponse, GraphSearchRequest,
GraphSearchResponse, NodeEdgeQueryParams, NodeListResponse, NodePayloadResponse,
ParallelTraverseRequest, StreamDoneEvent, StreamNodeEvent, StreamStatsEvent,
StreamTraverseParams, TraversalResultItem, TraversalStats, TraverseRequest, TraverseResponse,
UpsertNodePayloadRequest,
};
#[cfg(feature = "prometheus")]
pub use handlers::metrics::{health_metrics, prometheus_metrics};
#[derive(OpenApi)]
#[openapi(
info(
title = "VelesDB API",
version = env!("CARGO_PKG_VERSION"),
description = "High-performance vector database for AI applications. \
Supports semantic search, HNSW indexing, and multiple distance metrics. \
Authentication is optional — when API keys are configured via VELESDB_API_KEYS, \
all endpoints except /health and /ready require a valid Bearer token.",
license(name = "VelesDB Core License 1.0", url = "https://github.com/cyberlife-coder/VelesDB/blob/main/LICENSE"),
contact(name = "VelesDB Team", url = "https://github.com/cyberlife-coder/VelesDB")
),
security(
("bearer_auth" = [])
),
modifiers(&SecurityAddon),
servers(
(url = "/", description = "Local server")
),
tags(
(name = "health", description = "Health check endpoints"),
(name = "collections", description = "Collection management"),
(name = "points", description = "Vector point operations"),
(name = "search", description = "Vector similarity search"),
(name = "query", description = "VelesQL query execution"),
(name = "indexes", description = "Property index management (EPIC-009)"),
(name = "graph", description = "Graph traversal and edge operations"),
(name = "guardrails", description = "Query guard-rails configuration (EPIC-048)")
),
paths(
handlers::health::health_check,
handlers::health::readiness_check,
handlers::collections::list_collections,
handlers::collections::create_collection,
handlers::collections::get_collection,
handlers::collections::delete_collection,
handlers::collections::collection_sanity,
handlers::collections::is_empty,
handlers::collections::flush_collection,
handlers::admin::analyze_collection,
handlers::admin::get_collection_stats,
handlers::admin::get_guardrails,
handlers::admin::update_guardrails,
handlers::points::upsert_points,
handlers::points::stream_upsert_points,
handlers::points::stream_insert,
handlers::points::get_point,
handlers::points::delete_point,
handlers::points::scroll_points,
handlers::search::search,
handlers::search::batch_search,
handlers::search::multi_query_search,
handlers::search::text_search,
handlers::search::hybrid_search,
handlers::search::search_ids,
handlers::admin::get_collection_config,
handlers::query::query,
handlers::query::aggregate,
handlers::query::explain,
handlers::indexes::create_index,
handlers::indexes::list_indexes,
handlers::indexes::delete_index,
handlers::graph::handlers::get_edges,
handlers::graph::handlers::add_edge,
handlers::graph::handlers_extended::remove_edge,
handlers::graph::handlers_extended::get_edge_count,
handlers::graph::handlers_extended::list_nodes,
handlers::graph::handlers_extended::get_node_edges,
handlers::graph::handlers_extended::get_node_payload,
handlers::graph::handlers_extended::upsert_node_payload,
handlers::graph::handlers::traverse_graph,
handlers::graph::handlers_extended::traverse_parallel,
handlers::graph::handlers::get_node_degree,
handlers::graph::handlers_extended::graph_search,
handlers::graph::stream::stream_traverse,
handlers::match_query::match_query,
handlers::admin::rebuild_index,
handlers::admin::vacuum_collection,
handlers::admin::compact_collection,
handlers::points::bulk_delete_points
),
components(
schemas(
CreateCollectionRequest,
CollectionResponse,
UpsertPointsRequest,
PointRequest,
StreamInsertRequest,
SearchRequest,
BatchSearchRequest,
TextSearchRequest,
HybridSearchRequest,
MultiQuerySearchRequest,
SearchResponse,
BatchSearchResponse,
SearchResultResponse,
SearchIdsResponse,
IdScoreResult,
CollectionConfigResponse,
ErrorResponse,
QueryRequest,
QueryResponse,
QueryResponseMeta,
AggregationResponse,
QueryErrorResponse,
QueryErrorDetail,
VelesqlErrorResponse,
VelesqlErrorDetail,
ExplainRequest,
ExplainResponse,
ExplainStep,
ExplainCost,
ExplainFeatures,
ActualStatsResponse,
NodeStatsResponse,
CreateIndexRequest,
IndexResponse,
ListIndexesResponse,
CollectionStatsResponse,
ColumnStatsResponse,
IndexStatsResponse,
ScrollRequest,
ScrollResponse,
ScrollPoint,
GuardRailsConfigRequest,
GuardRailsConfigResponse,
handlers::graph::TraverseRequest,
handlers::graph::TraverseResponse,
handlers::graph::TraversalResultItem,
handlers::graph::TraversalStats,
handlers::graph::DegreeResponse,
handlers::graph::AddEdgeRequest,
handlers::graph::EdgesResponse,
handlers::graph::EdgeResponse,
handlers::graph::EdgeCountResponse,
handlers::graph::NodeListResponse,
handlers::graph::NodePayloadResponse,
handlers::graph::UpsertNodePayloadRequest,
handlers::graph::ParallelTraverseRequest,
handlers::graph::GraphSearchRequest,
handlers::graph::GraphSearchResponse,
handlers::graph::GraphSearchResultItem,
handlers::graph::StreamNodeEvent,
handlers::graph::StreamStatsEvent,
handlers::graph::StreamDoneEvent,
handlers::match_query::MatchQueryRequest,
handlers::match_query::MatchQueryResponse,
handlers::match_query::MatchQueryResultItem,
handlers::match_query::MatchQueryMeta,
handlers::match_query::MatchQueryError,
handlers::points::BulkDeleteRequest
)
)
)]
pub struct ApiDoc;
pub struct AppState {
pub db: Database,
pub onboarding_metrics: onboarding::OnboardingMetrics,
pub query_limits: parking_lot::RwLock<QueryLimits>,
pub ready: AtomicBool,
pub operational_metrics: Arc<OperationalMetrics>,
pub traversal_metrics: Arc<TraversalMetrics>,
pub query_duration_histogram: Arc<DurationHistogram>,
}
#[cfg(test)]
mod tests {
use super::*;
use utoipa::OpenApi;
#[test]
fn test_openapi_spec_generation() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
assert!(!json.is_empty(), "OpenAPI spec should not be empty");
assert!(json.contains("VelesDB API"), "Should contain API title");
assert!(
json.contains(env!("CARGO_PKG_VERSION")),
"Should contain version"
);
}
#[test]
fn test_openapi_has_all_endpoints() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
assert!(json.contains("/health"), "Should document /health");
assert!(
json.contains("/collections"),
"Should document /collections"
);
assert!(
json.contains(r"/collections/{name}"),
"Should document collections by name"
);
assert!(json.contains("/points"), "Should document points endpoint");
assert!(
json.contains(r"/collections/{name}/points/stream"),
"Should document points stream endpoint"
);
assert!(json.contains("/search"), "Should document search endpoint");
assert!(json.contains("/query"), "Should document /query");
assert!(json.contains("/aggregate"), "Should document /aggregate");
assert!(
json.contains("/query/explain"),
"Should document /query/explain"
);
}
#[test]
fn test_openapi_has_all_tags() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
assert!(json.contains("\"health\""), "Should have health tag");
assert!(
json.contains("\"collections\""),
"Should have collections tag"
);
assert!(json.contains("\"points\""), "Should have points tag");
assert!(json.contains("\"search\""), "Should have search tag");
assert!(json.contains("\"query\""), "Should have query tag");
}
#[test]
fn test_openapi_has_schemas() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
assert!(
json.contains("CreateCollectionRequest"),
"Should have CreateCollectionRequest schema"
);
assert!(
json.contains("CollectionResponse"),
"Should have CollectionResponse schema"
);
assert!(
json.contains("SearchRequest"),
"Should have SearchRequest schema"
);
assert!(
json.contains("SearchResponse"),
"Should have SearchResponse schema"
);
assert!(
json.contains("ErrorResponse"),
"Should have ErrorResponse schema"
);
}
#[test]
fn generate_openapi_spec_files() {
let openapi = ApiDoc::openapi();
let json = openapi
.to_pretty_json()
.expect("Failed to serialize OpenAPI JSON");
let yaml = serde_yaml::to_string(&openapi).expect("Failed to serialize OpenAPI YAML");
let docs_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("docs");
std::fs::create_dir_all(&docs_dir).expect("Failed to create docs dir");
std::fs::write(docs_dir.join("openapi.json"), &json).expect("Failed to write openapi.json");
std::fs::write(docs_dir.join("openapi.yaml"), &yaml).expect("Failed to write openapi.yaml");
assert!(
json.contains("sparse"),
"OpenAPI spec should contain sparse endpoints"
);
assert!(
json.contains("/graph/edges"),
"Should contain graph edge endpoints"
);
assert!(
json.contains("/graph/traverse"),
"Should contain graph traverse endpoint"
);
assert!(
json.contains("/stream/insert"),
"Should contain stream insert endpoint"
);
assert!(
json.contains("/match"),
"Should contain match query endpoint"
);
assert!(
json.contains("/search/multi"),
"Should contain multi-query search endpoint"
);
}
#[test]
fn test_openapi_has_license() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
assert!(
json.contains("VelesDB Core License 1.0"),
"Should have VelesDB Core License 1.0"
);
}
#[test]
fn test_openapi_pretty_json() {
let openapi = ApiDoc::openapi();
let pretty_json = openapi
.to_pretty_json()
.expect("Failed to serialize pretty JSON");
assert!(
pretty_json.contains('\n'),
"Pretty JSON should have newlines"
);
assert!(
pretty_json.len() > 1000,
"OpenAPI spec should be substantial"
);
}
#[test]
fn test_openapi_has_all_metrics_documented() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
assert!(json.contains("cosine"), "Should document cosine metric");
assert!(
json.contains("euclidean"),
"Should document euclidean metric"
);
assert!(json.contains("dot"), "Should document dot product metric");
assert!(json.contains("hamming"), "Should document hamming metric");
assert!(json.contains("jaccard"), "Should document jaccard metric");
}
#[test]
fn test_openapi_has_storage_mode_documented() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
assert!(
json.contains("storage_mode"),
"Should document storage_mode parameter"
);
}
#[test]
fn test_openapi_has_search_types_documented() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
assert!(json.contains("text_search"), "Should document text search");
assert!(
json.contains("hybrid_search"),
"Should document hybrid search"
);
assert!(json.contains("batch"), "Should document batch search");
}
#[test]
fn test_create_collection_request_default_metric() {
let json = r#"{"name": "test", "dimension": 128}"#;
let req: CreateCollectionRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.metric, "cosine");
}
#[test]
fn test_create_collection_request_with_hamming() {
let json = r#"{"name": "test", "dimension": 128, "metric": "hamming"}"#;
let req: CreateCollectionRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.metric, "hamming");
}
#[test]
fn test_create_collection_request_with_jaccard() {
let json = r#"{"name": "test", "dimension": 128, "metric": "jaccard"}"#;
let req: CreateCollectionRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.metric, "jaccard");
}
#[test]
fn test_create_collection_request_with_storage_mode() {
let json = r#"{"name": "test", "dimension": 128, "storage_mode": "sq8"}"#;
let req: CreateCollectionRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.storage_mode, "sq8");
}
#[test]
fn test_search_request_deserialize() {
let json = r#"{"vector": [0.1, 0.2, 0.3], "top_k": 5}"#;
let req: SearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.vector, vec![0.1, 0.2, 0.3]);
assert_eq!(req.top_k, 5);
}
#[test]
fn test_batch_search_request_deserialize() {
let json = r#"{"searches": [{"vector": [0.1, 0.2], "top_k": 3}]}"#;
let req: BatchSearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.searches.len(), 1);
assert_eq!(req.searches[0].top_k, 3);
}
#[test]
fn test_text_search_request_deserialize() {
let json = r#"{"query": "machine learning", "top_k": 10}"#;
let req: TextSearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.query, "machine learning");
assert_eq!(req.top_k, 10);
}
#[test]
fn test_hybrid_search_request_deserialize() {
let json = r#"{"vector": [0.1, 0.2], "query": "test", "top_k": 5}"#;
let req: HybridSearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.vector, vec![0.1, 0.2]);
assert_eq!(req.query, "test");
assert_eq!(req.top_k, 5);
}
#[test]
fn test_upsert_points_request_deserialize() {
let json = r#"{"points": [{"id": 1, "vector": [0.1, 0.2]}]}"#;
let req: UpsertPointsRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.points.len(), 1);
assert_eq!(req.points[0].id, 1);
}
#[test]
fn test_collection_response_serialize() {
let resp = CollectionResponse {
name: "test".to_string(),
dimension: 128,
metric: "cosine".to_string(),
storage_mode: "full".to_string(),
point_count: 100,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"name\":\"test\""));
assert!(json.contains("\"dimension\":128"));
assert!(json.contains("\"metric\":\"cosine\""));
assert!(json.contains("\"storage_mode\":\"full\""));
assert!(json.contains("\"point_count\":100"));
}
#[test]
fn test_search_response_serialize() {
let resp = SearchResponse {
results: vec![SearchResultResponse {
id: 1,
score: 0.95,
payload: None,
}],
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"results\""));
assert!(json.contains("\"id\":\"1\""));
}
#[test]
fn test_error_response_serialize() {
let resp = ErrorResponse {
error: "Test error".to_string(),
code: None,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"error\":\"Test error\""));
assert!(!json.contains("\"code\""));
}
fn extract_openapi_operations() -> Vec<(String, axum::http::Method)> {
let openapi = ApiDoc::openapi();
let mut ops = Vec::new();
for (path, item) in &openapi.paths.paths {
if item.get.is_some() {
ops.push((path.clone(), axum::http::Method::GET));
}
if item.post.is_some() {
ops.push((path.clone(), axum::http::Method::POST));
}
if item.put.is_some() {
ops.push((path.clone(), axum::http::Method::PUT));
}
if item.delete.is_some() {
ops.push((path.clone(), axum::http::Method::DELETE));
}
if item.patch.is_some() {
ops.push((path.clone(), axum::http::Method::PATCH));
}
}
ops.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.as_str().cmp(b.1.as_str())));
ops
}
fn template_to_uri(template: &str) -> String {
template
.replace("{name}", "test_col")
.replace("{id}", "1")
.replace("{node_id}", "1")
.replace("{edge_id}", "1")
.replace("{label}", "test_label")
.replace("{property}", "test_prop")
}
fn create_conformance_state() -> (std::sync::Arc<AppState>, tempfile::TempDir) {
let dir = tempfile::TempDir::new().expect("test: create temp dir");
let db = Database::open(dir.path()).expect("test: open database");
let state = std::sync::Arc::new(AppState {
db,
onboarding_metrics: OnboardingMetrics::default(),
query_limits: parking_lot::RwLock::new(QueryLimits::default()),
ready: AtomicBool::new(true),
operational_metrics: velesdb_core::metrics::OperationalMetrics::new_arc(),
traversal_metrics: Arc::new(velesdb_core::metrics::TraversalMetrics::new()),
query_duration_histogram: Arc::new(velesdb_core::metrics::DurationHistogram::new()),
});
(state, dir)
}
async fn is_axum_fallback(resp: axum::http::Response<axum::body::Body>) -> bool {
if resp.status() != axum::http::StatusCode::NOT_FOUND {
return false;
}
let body = axum::body::to_bytes(resp.into_body(), 1024 * 1024)
.await
.expect("test: read response body");
body.is_empty()
}
#[tokio::test]
async fn test_openapi_routes_match_router() {
let operations = extract_openapi_operations();
assert!(
!operations.is_empty(),
"OpenAPI spec should declare at least one operation"
);
let (state, _dir) = create_conformance_state();
let router = crate::routes::api_routes().with_state(state);
let mut failures: Vec<String> = Vec::new();
for (template, method) in &operations {
let uri = template_to_uri(template);
let req = axum::http::Request::builder()
.method(method)
.uri(&uri)
.header("content-type", "application/json")
.body(axum::body::Body::from("{}"))
.expect("test: build request");
let resp = tower::ServiceExt::oneshot(router.clone(), req)
.await
.expect("test: send request");
if is_axum_fallback(resp).await {
failures.push(format!("{method} {template}"));
}
}
assert!(
failures.is_empty(),
"OpenAPI operations with no matching router route:\n {}",
failures.join("\n ")
);
}
}