#![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)]
#![doc = include_str!("../README.md")]
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::{
Database, DurationHistogram, OperationalMetrics, QueryLimits, TraversalMetrics,
};
pub use onboarding::OnboardingMetrics;
pub use types::*;
pub use handlers::{
aggregate, analyze_collection, batch_search, bulk_delete_points, collection_diagnostics,
collection_sanity, compact_collection, create_collection, create_index, delete_collection,
delete_index, delete_point, enable_streaming, explain, flush_collection, get_collection,
get_collection_config, get_collection_stats, get_guardrails, get_point, get_point_relations,
health_check, hybrid_search, is_empty, list_collections, list_indexes, match_query,
multi_query_search, multi_query_search_ids, query, readiness_check, rebuild_index,
relate_points, reorder_for_locality, scroll_points, search, search_ids, set_point_ttl,
stream_insert, stream_upsert_points, text_search, unrelate_points, update_guardrails,
upsert_points, upsert_points_raw, vacuum_collection,
};
pub use handlers::graph::{
add_edge, add_edges_batch, 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)"),
(name = "metrics", description = "Prometheus operational metrics")
),
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::collection_diagnostics,
handlers::admin::get_guardrails,
handlers::admin::update_guardrails,
handlers::points::upsert_points,
handlers::points::raw::upsert_points_raw,
handlers::points::stream_upsert_points,
handlers::points::stream_insert,
handlers::points::enable_streaming,
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::multi_query_search_ids,
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::add_edges_batch,
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::admin::reorder_for_locality,
handlers::points::bulk_delete_points,
handlers::points::relations::relate_points,
handlers::points::relations::unrelate_points,
handlers::points::relations::get_point_relations,
handlers::points::relations::set_point_ttl,
),
components(
schemas(
CreateCollectionRequest,
CollectionResponse,
UpsertPointsRequest,
PointRequest,
StreamInsertRequest,
EnableStreamingRequest,
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,
CollectionDiagnosticsResponse,
handlers::graph::TraverseRequest,
handlers::graph::TraverseResponse,
handlers::graph::TraversalResultItem,
handlers::graph::TraversalStats,
handlers::graph::DegreeResponse,
handlers::graph::AddEdgeRequest,
handlers::graph::AddEdgesBatchRequest,
handlers::graph::AddEdgesBatchResponse,
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::points::BulkDeleteRequest,
handlers::points::relations::RelateRequest,
handlers::points::relations::RelateResponse,
handlers::points::relations::RelationEdge,
handlers::points::relations::RelationsResponse,
handlers::points::relations::SetTtlRequest
)
)
)]
struct ApiDocBase;
#[cfg(feature = "prometheus")]
#[derive(OpenApi)]
#[openapi(paths(handlers::metrics::prometheus_metrics))]
struct MetricsApiDoc;
pub struct ApiDoc;
impl ApiDoc {
pub fn openapi() -> utoipa::openapi::OpenApi {
#[allow(unused_mut)]
let mut doc = ApiDocBase::openapi();
#[cfg(feature = "prometheus")]
{
doc = doc.merge_from(MetricsApiDoc::openapi());
}
doc
}
}
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::*;
#[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"
);
}
fn update_openapi_snapshot_requested() -> bool {
std::env::var_os("UPDATE_OPENAPI_SNAPSHOT").is_some()
}
#[test]
#[ignore = "run explicitly via the openapi-drift CI job; see comment above"]
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()
.expect("test: CARGO_MANIFEST_DIR has a parent (crates/)")
.parent()
.expect("test: crates/ has a parent (workspace root)")
.join("docs");
let json_path = docs_dir.join("openapi.json");
let yaml_path = docs_dir.join("openapi.yaml");
if update_openapi_snapshot_requested() {
std::fs::create_dir_all(&docs_dir).expect("Failed to create docs dir");
std::fs::write(&json_path, &json).expect("Failed to write openapi.json");
std::fs::write(&yaml_path, &yaml).expect("Failed to write openapi.yaml");
} else {
let committed_json = std::fs::read_to_string(&json_path)
.expect("Failed to read docs/openapi.json (run with UPDATE_OPENAPI_SNAPSHOT=1 to create it)");
let committed_yaml = std::fs::read_to_string(&yaml_path)
.expect("Failed to read docs/openapi.yaml (run with UPDATE_OPENAPI_SNAPSHOT=1 to create it)");
assert_eq!(
json, committed_json,
"docs/openapi.json is stale — rerun with UPDATE_OPENAPI_SNAPSHOT=1 to regenerate"
);
assert_eq!(
yaml, committed_yaml,
"docs/openapi.yaml is stale — rerun with UPDATE_OPENAPI_SNAPSHOT=1 to regenerate"
);
}
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).expect("test: valid CreateCollectionRequest JSON");
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).expect("test: valid CreateCollectionRequest JSON");
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).expect("test: valid CreateCollectionRequest JSON");
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).expect("test: valid CreateCollectionRequest JSON");
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).expect("test: valid SearchRequest JSON");
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).expect("test: valid BatchSearchRequest JSON");
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).expect("test: valid TextSearchRequest JSON");
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).expect("test: valid HybridSearchRequest JSON");
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).expect("test: valid UpsertPointsRequest JSON");
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).expect("test: serialize CollectionResponse");
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).expect("test: serialize SearchResponse");
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).expect("test: serialize ErrorResponse");
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 ")
);
}
}