title: "Database API Reference" description: "API documentation for Navius database service and operations" category: api tags:
- api
- database
- storage
- repository related:
- ../patterns/database-service-pattern.md
- ../../02_examples/database-service-example.md
- ../patterns/repository-pattern.md last_updated: March 31, 2025 version: 1.0
Database API Reference
Overview
The Database API provides a generic interface for interacting with databases through the Database Service. While this is primarily a programmatic API rather than a REST API, this reference documents the core interfaces, operations, and usage patterns for working with the Database Service.
Core Interfaces
DatabaseOperations
The DatabaseOperations
trait defines the core operations available for all database implementations:
#![allow(unused)] fn main() { #[async_trait] pub trait DatabaseOperations: Send + Sync { /// Get a value from the database async fn get(&self, collection: &str, key: &str) -> Result<Option<String>, ServiceError>; /// Set a value in the database async fn set(&self, collection: &str, key: &str, value: &str) -> Result<(), ServiceError>; /// Delete a value from the database async fn delete(&self, collection: &str, key: &str) -> Result<bool, ServiceError>; /// Query the database with a filter async fn query(&self, collection: &str, filter: &str) -> Result<Vec<String>, ServiceError>; /// Execute a database transaction with multiple operations async fn transaction<F, T>(&self, operations: F) -> Result<T, ServiceError> where F: FnOnce(&dyn DatabaseOperations) -> Result<T, ServiceError> + Send + 'static, T: Send + 'static; } }
DatabaseProvider
The DatabaseProvider
trait enables creating database instances:
#![allow(unused)] fn main() { #[async_trait] pub trait DatabaseProvider: Send + Sync { /// The type of database this provider creates type Database: DatabaseOperations; /// Create a new database instance async fn create_database(&self, config: DatabaseConfig) -> Result<Self::Database, ServiceError>; /// Check if this provider supports the given configuration fn supports(&self, config: &DatabaseConfig) -> bool; /// Get the name of this provider fn name(&self) -> &str; } }
DatabaseService
The DatabaseService
manages database instances:
#![allow(unused)] fn main() { pub struct DatabaseService { provider_registry: Arc<RwLock<DatabaseProviderRegistry>>, default_config: DatabaseConfig, } impl DatabaseService { pub fn new(registry: DatabaseProviderRegistry) -> Self { // Implementation details... } pub fn with_default_config(mut self, config: DatabaseConfig) -> Self { // Implementation details... } pub async fn create_database(&self) -> Result<Box<dyn DatabaseOperations>, ServiceError> { // Implementation details... } } }
Using the Database API
Accessing the Database Service
The database service is accessible through the application's service registry:
#![allow(unused)] fn main() { use crate::core::services::ServiceRegistry; use crate::core::services::database_service::DatabaseService; // Get the service from service registry let db_service = service_registry.get::<DatabaseService>(); // Create a database instance let db = db_service.create_database().await?; }
Basic CRUD Operations
Creating/Updating Records
#![allow(unused)] fn main() { // Create a new user record let user = User { id: "user-123", name: "Alice", email: "[email protected]", }; // Serialize to JSON let user_json = serde_json::to_string(&user)?; // Store in database db.set("users", &user.id, &user_json).await?; }
Reading Records
#![allow(unused)] fn main() { // Get a user by ID if let Some(user_json) = db.get("users", "user-123").await? { // Deserialize from JSON let user: User = serde_json::from_str(&user_json)?; println!("Found user: {}", user.name); } }
Querying Records
#![allow(unused)] fn main() { // Query users with role=admin let admin_users = db.query("users", "role='admin'").await?; // Process results for user_json in admin_users { let user: User = serde_json::from_str(&user_json)?; println!("Admin user: {}", user.name); } }
Deleting Records
#![allow(unused)] fn main() { // Delete a user let deleted = db.delete("users", "user-123").await?; if deleted { println!("User deleted successfully"); } else { println!("User not found"); } }
Transactions
Transactions allow multiple operations to be executed atomically:
#![allow(unused)] fn main() { // Execute a transaction db.transaction(|tx| { // Create a new user tx.set("users", "user-1", r#"{"name":"Alice","balance":0}"#)?; // Create an account for the user tx.set("accounts", "account-1", r#"{"owner":"user-1","balance":100}"#)?; // Create initial transaction record tx.set("transactions", "tx-1", r#"{"account":"account-1","amount":100,"type":"deposit"}"#)?; Ok(()) }).await?; }
Using Repository Pattern
The Database API is typically used via the Repository pattern:
#![allow(unused)] fn main() { use crate::core::models::{Entity, Repository}; use crate::core::services::repository_service::GenericRepository; // Create a repository for User entities let user_repo = GenericRepository::<User>::with_service(&repository_service).await?; // Create a new user let mut user = User::new("Alice", "[email protected]"); // Save the user let saved_user = user_repo.save(&user).await?; // Find a user by ID if let Some(found_user) = user_repo.find_by_id(&user.id).await? { println!("Found user: {}", found_user.name); } // Delete a user let deleted = user_repo.delete(&user.id).await?; }
Available Database Providers
InMemoryDatabaseProvider
The in-memory database provider is useful for development and testing:
#![allow(unused)] fn main() { use crate::core::services::memory_database::InMemoryDatabaseProvider; // Create a provider let provider = InMemoryDatabaseProvider::new(); // Create a database instance let config = DatabaseConfig::default(); let db = provider.create_database(config).await?; }
PostgresDatabaseProvider
When PostgreSQL integration is enabled, the PostgreSQL provider is available:
#![allow(unused)] fn main() { use crate::core::services::postgres_database::PostgresDatabaseProvider; // Create a provider with connection string let provider = PostgresDatabaseProvider::new("postgres://user:pass@localhost/dbname"); // Create a database instance let config = DatabaseConfig::default(); let db = provider.create_database(config).await?; }
Configuration
The Database Service can be configured in config/default.yaml
:
# Database configuration
database:
# Default provider to use
provider: memory
# Provider-specific configurations
providers:
memory:
enabled: true
postgres:
enabled: true
connection_string: "postgres://user:pass@localhost/dbname"
max_connections: 10
connection_timeout_ms: 5000
idle_timeout_ms: 300000
# Common settings
common:
query_timeout_ms: 3000
log_queries: true
Error Handling
The Database API uses ServiceError
for error handling:
#![allow(unused)] fn main() { // Example error handling match db.get("users", "user-123").await { Ok(Some(user_json)) => { // Process user }, Ok(None) => { // User not found println!("User not found"); }, Err(e) => { match e { ServiceError::DatabaseError { message, .. } => { // Handle database error println!("Database error: {}", message); }, ServiceError::NotFound { message } => { // Handle not found error println!("Not found: {}", message); }, _ => { // Handle other errors println!("Error: {}", e); } } } } }
Implementing a Custom Provider
You can implement your own database provider by implementing the DatabaseProvider
trait:
#![allow(unused)] fn main() { use crate::core::services::database_service::{ DatabaseOperations, DatabaseProvider, DatabaseConfig }; use crate::core::services::error::ServiceError; use async_trait::async_trait; // Custom database implementation pub struct CustomDatabase { // Implementation details... } #[async_trait] impl DatabaseOperations for CustomDatabase { async fn get(&self, collection: &str, key: &str) -> Result<Option<String>, ServiceError> { // Implementation... } // Other methods... } // Custom provider pub struct CustomDatabaseProvider { // Provider details... } #[async_trait] impl DatabaseProvider for CustomDatabaseProvider { type Database = CustomDatabase; async fn create_database(&self, config: DatabaseConfig) -> Result<Self::Database, ServiceError> { // Implementation... } fn supports(&self, config: &DatabaseConfig) -> bool { config.provider_type == "custom" } fn name(&self) -> &str { "custom" } } }
Register your custom provider:
#![allow(unused)] fn main() { let mut registry = DatabaseProviderRegistry::new(); registry.register("custom", CustomDatabaseProvider::new()); let service = DatabaseService::new(registry); }
Best Practices
Collection Naming
- Use lowercase, plural nouns for collection names (e.g.,
users
,accounts
) - Use dashes instead of spaces or underscores (e.g.,
order-items
) - Keep collection names consistent across the application
Key Generation
- Use UUIDs or other globally unique identifiers for keys
- Consider using prefixed keys for better organization (e.g.,
user-123
) - Be consistent with key formats within each collection
JSON Serialization
- Use serde for JSON serialization/deserialization
- Define clear schema for each collection's documents
- Include version information in documents for schema evolution
- Consider using compression for large documents
Query Patterns
- Keep queries simple and specific to indexes
- Use appropriate filters to minimize data transfer
- Consider pagination for large result sets
- Use transactions for operations that must be atomic
Error Handling
- Handle both expected errors (e.g., not found) and unexpected errors
- Provide appropriate context in error messages
- Consider retrying transient errors (e.g., connection issues)
- Don't expose internal database errors to users
Performance Considerations
- Use connection pooling for database connections
- Cache frequently accessed data
- Use batch operations for multiple records
- Consider data access patterns when designing schemas
- Use appropriate indexes for frequent queries
- Monitor query performance and optimize as needed