Skip to main content

velesdb_core/observer/
mod.rs

1//! `DatabaseObserver` — extension hook for velesdb-premium.
2//!
3//! The core library has zero knowledge of Premium internals.
4//! Premium implements this trait and injects it via
5//! [`Database::open_with_observer`](crate::Database::open_with_observer).
6//!
7//! # Contract
8//!
9//! - All methods have default no-op implementations.
10//! - Implementations MUST be `Send + Sync`.
11//! - Implementations MUST NOT panic.
12//! - Overhead when `observer` is `None` is a single pointer check.
13
14pub mod context;
15
16#[cfg(test)]
17mod backward_compat_tests;
18
19#[cfg(test)]
20mod context_tests;
21
22pub use context::{AccessDecision, AccessScope, QueryAccessContext, QueryOperationKind};
23
24use crate::collection::CollectionType;
25
26/// Lifecycle and control-plane hooks for database events.
27///
28/// Implement this trait in `velesdb-premium` to attach RBAC, audit logging,
29/// multi-tenant routing, or replication logic without modifying the core.
30///
31/// # Contract
32///
33/// - All methods have default implementations: telemetry/lifecycle hooks are
34///   no-ops and gate hooks are allow-all. A consumer that supplies no observer,
35///   or an observer overriding only a subset of methods, behaves exactly as it
36///   did before any hook existed.
37/// - Implementations MUST be `Send + Sync`.
38/// - Implementations MUST NOT panic.
39/// - Access denial flows through a decision value
40///   ([`AccessDecision::Deny`]), **not** through the `Result` error channel:
41///   the `Err` variant of a gate is reserved for internal observer failures.
42///
43/// Every method has a default implementation, so adding future hooks is not a
44/// breaking change for downstream implementers — they keep compiling by
45/// relying on the defaults (the trait-level equivalent of `#[non_exhaustive]`,
46/// which Rust does not permit on traits directly).
47///
48/// # Example (Premium side)
49///
50/// ```rust,ignore
51/// use velesdb_core::{DatabaseObserver, CollectionType};
52///
53/// struct PremiumObserver { /* audit_log, rbac, tenant_router */ }
54///
55/// impl DatabaseObserver for PremiumObserver {
56///     fn on_collection_created(&self, name: &str, kind: &CollectionType) {
57///         // self.audit_log.record(...)
58///     }
59/// }
60/// ```
61pub trait DatabaseObserver: Send + Sync {
62    /// Called after a collection is successfully created.
63    fn on_collection_created(&self, _name: &str, _kind: &CollectionType) {}
64
65    /// Called after a collection is successfully deleted.
66    fn on_collection_deleted(&self, _name: &str) {}
67
68    /// Called after points are upserted into a collection.
69    fn on_upsert(&self, _collection: &str, _point_count: usize) {}
70
71    /// Called after a query is executed, with the duration in microseconds.
72    fn on_query(&self, _collection: &str, _duration_us: u64) {}
73
74    /// Called before a DDL statement is executed.
75    ///
76    /// Premium extensions can implement this to enforce RBAC policies
77    /// (e.g., only admin users can CREATE/DROP collections).
78    ///
79    /// Returns `Ok(())` to allow the DDL operation, or `Err(Error)` to reject it.
80    /// Default implementation allows all DDL operations.
81    ///
82    /// # Errors
83    ///
84    /// Implementations should return an error to reject the DDL operation.
85    fn on_ddl_request(&self, operation: &str, collection_name: &str) -> crate::Result<()> {
86        let _ = (operation, collection_name);
87        Ok(())
88    }
89
90    /// Called before a mutating DML statement is executed.
91    ///
92    /// Premium extensions can implement this to enforce RBAC policies
93    /// (e.g., restrict INSERT EDGE, DELETE, or DELETE EDGE to authorized users).
94    ///
95    /// Returns `Ok(())` to allow the DML mutation, or `Err(Error)` to reject it.
96    /// Default implementation allows all DML mutations.
97    ///
98    /// # Errors
99    ///
100    /// Implementations should return an error to reject the DML mutation.
101    fn on_dml_mutation_request(&self, operation: &str, collection_name: &str) -> crate::Result<()> {
102        let _ = (operation, collection_name);
103        Ok(())
104    }
105
106    /// Called in the core use-case layer immediately before a query/read
107    /// executes, for every read path (vector search, text/BM25, hybrid,
108    /// graph traversal, `VelesQL` SELECT).
109    ///
110    /// Premium extensions can implement this to enforce RBAC, tenant scoping,
111    /// and row/collection filtering on the read path so that every consumer of
112    /// core inherits control-plane behavior through the port.
113    ///
114    /// Returns an [`AccessDecision`] the core enforces before producing
115    /// results: [`Allow`](AccessDecision::Allow) executes unmodified,
116    /// [`Deny`](AccessDecision::Deny) aborts with the supplied error and zero
117    /// results, and [`AllowWithScope`](AccessDecision::AllowWithScope) narrows
118    /// the query before execution. The default allows every read unmodified,
119    /// so the open path is unchanged.
120    ///
121    /// # Errors
122    ///
123    /// Implementations return `Err` only for internal failures; access denial
124    /// is expressed through [`AccessDecision::Deny`], not the `Result` error.
125    fn on_query_request(&self, ctx: &QueryAccessContext) -> crate::Result<AccessDecision> {
126        let _ = ctx;
127        Ok(AccessDecision::Allow)
128    }
129}