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}