From 3b8e029cde6063080422ab4a8d0dc0e74b8604ac Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Thu, 23 Apr 2026 09:22:11 +0200 Subject: [PATCH] 0.5.1 --- CHANGELOG.md | 1 + Cargo.toml | 2 +- ROADMAP.md | 55 ++++- kb_lib/src/db.rs | 15 ++ kb_lib/src/db/dtos.rs | 6 + kb_lib/src/db/dtos/db_runtime_event.rs | 69 +++++++ kb_lib/src/db/dtos/known_http_endpoint.rs | 121 +++++++++++ kb_lib/src/db/dtos/known_ws_endpoint.rs | 121 +++++++++++ kb_lib/src/db/entities.rs | 6 + kb_lib/src/db/entities/db_runtime_event.rs | 20 ++ kb_lib/src/db/entities/known_http_endpoint.rs | 22 ++ kb_lib/src/db/entities/known_ws_endpoint.rs | 22 ++ kb_lib/src/db/queries.rs | 11 + kb_lib/src/db/queries/db_runtime_event.rs | 133 ++++++++++++ kb_lib/src/db/queries/known_http_endpoint.rs | 195 ++++++++++++++++++ kb_lib/src/db/queries/known_ws_endpoint.rs | 195 ++++++++++++++++++ kb_lib/src/db/schema.rs | 82 +++++++- kb_lib/src/db/types.rs | 2 + kb_lib/src/db/types/runtime_event_level.rs | 46 +++++ kb_lib/src/lib.rs | 15 ++ 20 files changed, 1123 insertions(+), 16 deletions(-) create mode 100644 kb_lib/src/db/dtos/db_runtime_event.rs create mode 100644 kb_lib/src/db/dtos/known_http_endpoint.rs create mode 100644 kb_lib/src/db/dtos/known_ws_endpoint.rs create mode 100644 kb_lib/src/db/entities/db_runtime_event.rs create mode 100644 kb_lib/src/db/entities/known_http_endpoint.rs create mode 100644 kb_lib/src/db/entities/known_ws_endpoint.rs create mode 100644 kb_lib/src/db/queries/db_runtime_event.rs create mode 100644 kb_lib/src/db/queries/known_http_endpoint.rs create mode 100644 kb_lib/src/db/queries/known_ws_endpoint.rs create mode 100644 kb_lib/src/db/types/runtime_event_level.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ac7792e..fd96891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,3 +17,4 @@ 0.4.3 - Pool d’endpoints HTTP 0.4.4 - Ajout de la fenêtre Demo Http dans kb_app, exécution manuelle des méthodes HTTP via le pool, snapshot des endpoints et amélioration des presets UI 0.5.0 - Début du socle SQLite : configuration database, ouverture/validation de la base et premières briques de persistance +0.5.1 - Ajout des premières tables métier SQLite pour les endpoints connus et les événements runtime, avec séparation entities/dtos/queries/types diff --git a/Cargo.toml b/Cargo.toml index cdc7e0b..49a12a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.5.0" +version = "0.5.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" diff --git a/ROADMAP.md b/ROADMAP.md index cc9f825..b85bba5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -332,16 +332,49 @@ Réalisé : - alignement visuel de la fenêtre sur le gabarit `Demo Ws`, - amélioration des presets UI, copie de réponse et bascule pretty/raw. -### 6.12. Version `0.5.x` — Base de données SQLite +## 6.12. Version `0.5.x` — Base de données SQLite -Objectif : poser la persistance locale. +Objectif : poser la persistance locale avec une organisation préparée dès le départ à une future évolution vers PostgreSQL ou un autre backend. -À faire : +#### 0.5.0 — Socle SQLite +Réalisé : - configuration DB dans `config.json`, - ouverture/validation SQLite, -- premières tables techniques, -- stockage des endpoints, événements, tokens observés, subscriptions actives si utile. +- façade `KbDatabase`, +- premier schéma technique, +- table `kb_db_metadata`, +- séparation `db/entities`, `db/dtos`, `db/queries`, `db/types`. + +#### 0.5.1 — Premières tables métier de stockage local +À faire : + +- ajouter les tables de référence pour les endpoints connus, +- ajouter les tables techniques pour les événements runtime locaux, +- poser les `entities`, `dtos`, `queries` et `types` associés, +- préparer le stockage local des endpoints HTTP/WS connus et de leur état utile. + +#### 0.5.2 — Stockage des tokens observés +À faire : + +- ajouter les premières tables liées aux tokens observés, +- préparer le stockage minimal des mints, symboles, statuts et dates de découverte, +- préparer les relations futures avec pools, paires et événements on-chain. + +#### 0.5.3 — Événements et signaux locaux +À faire : + +- stocker les événements techniques importants remontés par les connecteurs, +- préparer la conservation locale des signaux utiles à l’analyse, +- distinguer événements runtime, observations on-chain et événements métier. + +#### 0.5.x — Consolidation de la couche stockage +À faire : + +- conserver l’abstraction du backend dès le départ, +- limiter la dépendance directe au SQL concret aux modules `queries`, +- garder les conversions explicites entre entités DB et DTOs applicatifs, +- préparer une future compatibilité PostgreSQL sans casser l’organisation générale. ### 6.13. Version `0.6.x` — Détection technique on-chain / RPC @@ -503,9 +536,9 @@ Le projet doit maintenir au minimum : La priorité immédiate est désormais la suivante : -1. démarrer la version `0.5.x` avec le socle SQLite, -2. ajouter la configuration database dans `config.json`, -3. poser l’ouverture et la validation de la base SQLite, -4. définir les premières tables techniques utiles au stockage local, -5. préparer la persistance des endpoints, événements et tokens observés, -6. conserver `kb_lib` comme point central de la logique de stockage. +1. démarrer la version `0.5.1` avec les premières tables métier SQLite, +2. ajouter les tables locales pour les endpoints connus HTTP/WS, +3. ajouter les tables locales pour les événements runtime techniques, +4. structurer ces tables via `db/entities`, `db/dtos`, `db/queries` et `db/types`, +5. conserver l’abstraction du backend dès cette phase SQLite, +6. préparer ensuite le stockage des tokens observés et des premiers signaux persistés. diff --git a/kb_lib/src/db.rs b/kb_lib/src/db.rs index f067c00..735bfde 100644 --- a/kb_lib/src/db.rs +++ b/kb_lib/src/db.rs @@ -16,8 +16,23 @@ mod types; pub use crate::db::connection::KbDatabase; pub use crate::db::connection::KbDatabaseConnection; pub use crate::db::dtos::KbDbMetadataDto; +pub use crate::db::dtos::KbDbRuntimeEventDto; +pub use crate::db::dtos::KbKnownHttpEndpointDto; +pub use crate::db::dtos::KbKnownWsEndpointDto; pub use crate::db::entities::KbDbMetadataEntity; +pub use crate::db::entities::KbDbRuntimeEventEntity; +pub use crate::db::entities::KbKnownHttpEndpointEntity; +pub use crate::db::entities::KbKnownWsEndpointEntity; pub use crate::db::queries::get_db_metadata; +pub use crate::db::queries::get_known_http_endpoint; +pub use crate::db::queries::get_known_ws_endpoint; +pub use crate::db::queries::insert_db_runtime_event; pub use crate::db::queries::list_db_metadata; +pub use crate::db::queries::list_known_http_endpoints; +pub use crate::db::queries::list_known_ws_endpoints; +pub use crate::db::queries::list_recent_db_runtime_events; pub use crate::db::queries::upsert_db_metadata; +pub use crate::db::queries::upsert_known_http_endpoint; +pub use crate::db::queries::upsert_known_ws_endpoint; pub use crate::db::types::KbDatabaseBackend; +pub use crate::db::types::KbDbRuntimeEventLevel; diff --git a/kb_lib/src/db/dtos.rs b/kb_lib/src/db/dtos.rs index d5b180e..13bc1e5 100644 --- a/kb_lib/src/db/dtos.rs +++ b/kb_lib/src/db/dtos.rs @@ -3,5 +3,11 @@ //! Database data transfer objects. mod db_metadata; +mod db_runtime_event; +mod known_http_endpoint; +mod known_ws_endpoint; pub use crate::db::dtos::db_metadata::KbDbMetadataDto; +pub use crate::db::dtos::db_runtime_event::KbDbRuntimeEventDto; +pub use crate::db::dtos::known_http_endpoint::KbKnownHttpEndpointDto; +pub use crate::db::dtos::known_ws_endpoint::KbKnownWsEndpointDto; diff --git a/kb_lib/src/db/dtos/db_runtime_event.rs b/kb_lib/src/db/dtos/db_runtime_event.rs new file mode 100644 index 0000000..a82f3d0 --- /dev/null +++ b/kb_lib/src/db/dtos/db_runtime_event.rs @@ -0,0 +1,69 @@ +// file: kb_lib/src/db/dtos/db_runtime_event.rs + +//! Runtime event DTO. + +/// Application-facing runtime event DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbDbRuntimeEventDto { + /// Optional numeric primary key. + pub id: std::option::Option, + /// Event kind. + pub event_kind: std::string::String, + /// Severity level. + pub level: crate::KbDbRuntimeEventLevel, + /// Event source. + pub source: std::string::String, + /// Human-readable message. + pub message: std::string::String, + /// Creation timestamp. + pub created_at: chrono::DateTime, +} + +impl KbDbRuntimeEventDto { + /// Creates a new runtime event DTO with the current creation timestamp. + pub fn new( + event_kind: std::string::String, + level: crate::KbDbRuntimeEventLevel, + source: std::string::String, + message: std::string::String, + ) -> Self { + Self { + id: None, + event_kind, + level, + source, + message, + created_at: chrono::Utc::now(), + } + } +} + +impl TryFrom for KbDbRuntimeEventDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbDbRuntimeEventEntity) -> Result { + let created_at_result = chrono::DateTime::parse_from_rfc3339(&entity.created_at); + let created_at = match created_at_result { + Ok(created_at) => created_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse runtime event created_at '{}': {}", + entity.created_at, error + ))); + } + }; + let level_result = crate::KbDbRuntimeEventLevel::from_i16(entity.level); + let level = match level_result { + Ok(level) => level, + Err(error) => return Err(error), + }; + Ok(Self { + id: Some(entity.id), + event_kind: entity.event_kind, + level, + source: entity.source, + message: entity.message, + created_at, + }) + } +} diff --git a/kb_lib/src/db/dtos/known_http_endpoint.rs b/kb_lib/src/db/dtos/known_http_endpoint.rs new file mode 100644 index 0000000..ff0d9ac --- /dev/null +++ b/kb_lib/src/db/dtos/known_http_endpoint.rs @@ -0,0 +1,121 @@ +// file: kb_lib/src/db/dtos/known_http_endpoint.rs + +//! Known HTTP endpoint DTO. + +/// Application-facing known HTTP endpoint DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbKnownHttpEndpointDto { + /// Logical endpoint name. + pub name: std::string::String, + /// Provider name. + pub provider: std::string::String, + /// Endpoint URL. + pub url: std::string::String, + /// Whether this endpoint is enabled. + pub enabled: bool, + /// Declared roles. + pub roles: std::vec::Vec, + /// Optional last seen timestamp. + pub last_seen_at: std::option::Option>, + /// Last update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbKnownHttpEndpointDto { + /// Creates a new DTO with the current update timestamp. + pub fn new( + name: std::string::String, + provider: std::string::String, + url: std::string::String, + enabled: bool, + roles: std::vec::Vec, + ) -> Self { + Self { + name, + provider, + url, + enabled, + roles, + last_seen_at: None, + updated_at: chrono::Utc::now(), + } + } +} + +impl TryFrom for KbKnownHttpEndpointDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbKnownHttpEndpointEntity) -> Result { + let roles_result = + serde_json::from_str::>(&entity.roles_json); + let roles = match roles_result { + Ok(roles) => roles, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse known http endpoint roles_json '{}': {}", + entity.roles_json, error + ))); + } + }; + let updated_at_result = chrono::DateTime::parse_from_rfc3339(&entity.updated_at); + let updated_at = match updated_at_result { + Ok(updated_at) => updated_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse known http endpoint updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + let last_seen_at = match entity.last_seen_at { + Some(last_seen_at_text) => { + let parsed_result = chrono::DateTime::parse_from_rfc3339(&last_seen_at_text); + match parsed_result { + Ok(parsed) => Some(parsed.with_timezone(&chrono::Utc)), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse known http endpoint last_seen_at '{}': {}", + last_seen_at_text, error + ))); + } + } + } + None => None, + }; + Ok(Self { + name: entity.name, + provider: entity.provider, + url: entity.url, + enabled: entity.enabled != 0, + roles, + last_seen_at, + updated_at, + }) + } +} + +impl TryFrom for crate::KbKnownHttpEndpointEntity { + type Error = crate::KbError; + + fn try_from(dto: KbKnownHttpEndpointDto) -> Result { + let roles_json_result = serde_json::to_string(&dto.roles); + let roles_json = match roles_json_result { + Ok(roles_json) => roles_json, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot serialize known http endpoint roles: {}", + error + ))); + } + }; + Ok(Self { + name: dto.name, + provider: dto.provider, + url: dto.url, + enabled: if dto.enabled { 1 } else { 0 }, + roles_json, + last_seen_at: dto.last_seen_at.map(|value| value.to_rfc3339()), + updated_at: dto.updated_at.to_rfc3339(), + }) + } +} diff --git a/kb_lib/src/db/dtos/known_ws_endpoint.rs b/kb_lib/src/db/dtos/known_ws_endpoint.rs new file mode 100644 index 0000000..8820af3 --- /dev/null +++ b/kb_lib/src/db/dtos/known_ws_endpoint.rs @@ -0,0 +1,121 @@ +// file: kb_lib/src/db/dtos/known_ws_endpoint.rs + +//! Known WebSocket endpoint DTO. + +/// Application-facing known WebSocket endpoint DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbKnownWsEndpointDto { + /// Logical endpoint name. + pub name: std::string::String, + /// Provider name. + pub provider: std::string::String, + /// Endpoint URL. + pub url: std::string::String, + /// Whether this endpoint is enabled. + pub enabled: bool, + /// Declared roles. + pub roles: std::vec::Vec, + /// Optional last seen timestamp. + pub last_seen_at: std::option::Option>, + /// Last update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbKnownWsEndpointDto { + /// Creates a new DTO with the current update timestamp. + pub fn new( + name: std::string::String, + provider: std::string::String, + url: std::string::String, + enabled: bool, + roles: std::vec::Vec, + ) -> Self { + Self { + name, + provider, + url, + enabled, + roles, + last_seen_at: None, + updated_at: chrono::Utc::now(), + } + } +} + +impl TryFrom for KbKnownWsEndpointDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbKnownWsEndpointEntity) -> Result { + let roles_result = + serde_json::from_str::>(&entity.roles_json); + let roles = match roles_result { + Ok(roles) => roles, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse known ws endpoint roles_json '{}': {}", + entity.roles_json, error + ))); + } + }; + let updated_at_result = chrono::DateTime::parse_from_rfc3339(&entity.updated_at); + let updated_at = match updated_at_result { + Ok(updated_at) => updated_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse known ws endpoint updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + let last_seen_at = match entity.last_seen_at { + Some(last_seen_at_text) => { + let parsed_result = chrono::DateTime::parse_from_rfc3339(&last_seen_at_text); + match parsed_result { + Ok(parsed) => Some(parsed.with_timezone(&chrono::Utc)), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse known ws endpoint last_seen_at '{}': {}", + last_seen_at_text, error + ))); + } + } + } + None => None, + }; + Ok(Self { + name: entity.name, + provider: entity.provider, + url: entity.url, + enabled: entity.enabled != 0, + roles, + last_seen_at, + updated_at, + }) + } +} + +impl TryFrom for crate::KbKnownWsEndpointEntity { + type Error = crate::KbError; + + fn try_from(dto: KbKnownWsEndpointDto) -> Result { + let roles_json_result = serde_json::to_string(&dto.roles); + let roles_json = match roles_json_result { + Ok(roles_json) => roles_json, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot serialize known ws endpoint roles: {}", + error + ))); + } + }; + Ok(Self { + name: dto.name, + provider: dto.provider, + url: dto.url, + enabled: if dto.enabled { 1 } else { 0 }, + roles_json, + last_seen_at: dto.last_seen_at.map(|value| value.to_rfc3339()), + updated_at: dto.updated_at.to_rfc3339(), + }) + } +} diff --git a/kb_lib/src/db/entities.rs b/kb_lib/src/db/entities.rs index 7e04f9d..01d1041 100644 --- a/kb_lib/src/db/entities.rs +++ b/kb_lib/src/db/entities.rs @@ -5,5 +5,11 @@ //! These types are close to persisted rows and SQL query results. mod db_metadata; +mod db_runtime_event; +mod known_http_endpoint; +mod known_ws_endpoint; pub use crate::db::entities::db_metadata::KbDbMetadataEntity; +pub use crate::db::entities::db_runtime_event::KbDbRuntimeEventEntity; +pub use crate::db::entities::known_http_endpoint::KbKnownHttpEndpointEntity; +pub use crate::db::entities::known_ws_endpoint::KbKnownWsEndpointEntity; diff --git a/kb_lib/src/db/entities/db_runtime_event.rs b/kb_lib/src/db/entities/db_runtime_event.rs new file mode 100644 index 0000000..05c3ed6 --- /dev/null +++ b/kb_lib/src/db/entities/db_runtime_event.rs @@ -0,0 +1,20 @@ +// file: kb_lib/src/db/entities/db_runtime_event.rs + +//! Runtime event entity. + +/// Persisted runtime event row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbDbRuntimeEventEntity { + /// Numeric primary key. + pub id: i64, + /// Event kind. + pub event_kind: std::string::String, + /// Severity level stored as stable integer. + pub level: i16, + /// Event source. + pub source: std::string::String, + /// Human-readable message. + pub message: std::string::String, + /// Creation timestamp encoded as RFC3339 UTC text. + pub created_at: std::string::String, +} diff --git a/kb_lib/src/db/entities/known_http_endpoint.rs b/kb_lib/src/db/entities/known_http_endpoint.rs new file mode 100644 index 0000000..8ae0a1c --- /dev/null +++ b/kb_lib/src/db/entities/known_http_endpoint.rs @@ -0,0 +1,22 @@ +// file: kb_lib/src/db/entities/known_http_endpoint.rs + +//! Known HTTP endpoint entity. + +/// Persisted known HTTP endpoint row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbKnownHttpEndpointEntity { + /// Logical endpoint name. + pub name: std::string::String, + /// Provider name. + pub provider: std::string::String, + /// Endpoint URL. + pub url: std::string::String, + /// Whether this endpoint is enabled. + pub enabled: i64, + /// JSON-encoded roles. + pub roles_json: std::string::String, + /// Optional last seen timestamp encoded as RFC3339 UTC text. + pub last_seen_at: std::option::Option, + /// Last update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/entities/known_ws_endpoint.rs b/kb_lib/src/db/entities/known_ws_endpoint.rs new file mode 100644 index 0000000..a33c1c5 --- /dev/null +++ b/kb_lib/src/db/entities/known_ws_endpoint.rs @@ -0,0 +1,22 @@ +// file: kb_lib/src/db/entities/known_ws_endpoint.rs + +//! Known WebSocket endpoint entity. + +/// Persisted known WebSocket endpoint row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbKnownWsEndpointEntity { + /// Logical endpoint name. + pub name: std::string::String, + /// Provider name. + pub provider: std::string::String, + /// Endpoint URL. + pub url: std::string::String, + /// Whether this endpoint is enabled. + pub enabled: i64, + /// JSON-encoded roles. + pub roles_json: std::string::String, + /// Optional last seen timestamp encoded as RFC3339 UTC text. + pub last_seen_at: std::option::Option, + /// Last update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/queries.rs b/kb_lib/src/db/queries.rs index 2d2a824..0e7a983 100644 --- a/kb_lib/src/db/queries.rs +++ b/kb_lib/src/db/queries.rs @@ -3,7 +3,18 @@ //! Database queries. mod db_metadata; +mod db_runtime_event; +mod known_http_endpoint; +mod known_ws_endpoint; pub use crate::db::queries::db_metadata::get_db_metadata; pub use crate::db::queries::db_metadata::list_db_metadata; pub use crate::db::queries::db_metadata::upsert_db_metadata; +pub use crate::db::queries::db_runtime_event::insert_db_runtime_event; +pub use crate::db::queries::db_runtime_event::list_recent_db_runtime_events; +pub use crate::db::queries::known_http_endpoint::get_known_http_endpoint; +pub use crate::db::queries::known_http_endpoint::list_known_http_endpoints; +pub use crate::db::queries::known_http_endpoint::upsert_known_http_endpoint; +pub use crate::db::queries::known_ws_endpoint::get_known_ws_endpoint; +pub use crate::db::queries::known_ws_endpoint::list_known_ws_endpoints; +pub use crate::db::queries::known_ws_endpoint::upsert_known_ws_endpoint; diff --git a/kb_lib/src/db/queries/db_runtime_event.rs b/kb_lib/src/db/queries/db_runtime_event.rs new file mode 100644 index 0000000..0fde3c6 --- /dev/null +++ b/kb_lib/src/db/queries/db_runtime_event.rs @@ -0,0 +1,133 @@ +// file: kb_lib/src/db/queries/db_runtime_event.rs + +//! Queries for `kb_db_runtime_events`. + +/// Inserts one runtime event row and returns its numeric id. +pub async fn insert_db_runtime_event( + database: &crate::KbDatabase, + dto: &crate::KbDbRuntimeEventDto, +) -> Result { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_db_runtime_events ( + event_kind, + level, + source, + message, + created_at +) +VALUES (?, ?, ?, ?, ?) + "#, + ) + .bind(dto.event_kind.clone()) + .bind(dto.level.to_i16()) + .bind(dto.source.clone()) + .bind(dto.message.clone()) + .bind(dto.created_at.to_rfc3339()) + .execute(pool) + .await; + let query_result = match query_result { + Ok(query_result) => query_result, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot insert kb_db_runtime_events on sqlite: {}", + error + ))); + } + }; + Ok(query_result.last_insert_rowid()) + } + } +} + +/// Lists recent runtime events ordered from newest to oldest. +pub async fn list_recent_db_runtime_events( + database: &crate::KbDatabase, + limit: u32, +) -> Result, crate::KbError> { + if limit == 0 { + return Ok(std::vec::Vec::new()); + } + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query_as::( + r#" +SELECT + id, + event_kind, + level, + source, + message, + created_at +FROM kb_db_runtime_events +ORDER BY id DESC +LIMIT ? + "#, + ) + .bind(i64::from(limit)) + .fetch_all(pool) + .await; + let entities = match query_result { + Ok(entities) => entities, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot list runtime events on sqlite: {}", + error + ))); + } + }; + let mut dtos = std::vec::Vec::new(); + for entity in entities { + let dto_result = crate::KbDbRuntimeEventDto::try_from(entity); + let dto = match dto_result { + Ok(dto) => dto, + Err(error) => return Err(error), + }; + dtos.push(dto); + } + Ok(dtos) + } + } +} + +#[cfg(test)] +mod tests { + #[tokio::test] + async fn runtime_event_roundtrip_works() { + let tempdir = tempfile::tempdir().expect("tempdir must succeed"); + let database_path = tempdir.path().join("runtime_event.sqlite3"); + let config = crate::KbDatabaseConfig { + enabled: true, + backend: crate::KbDatabaseBackend::Sqlite, + sqlite: crate::KbSqliteDatabaseConfig { + path: database_path.to_string_lossy().to_string(), + create_if_missing: true, + busy_timeout_ms: 5000, + max_connections: 1, + auto_initialize_schema: true, + use_wal: true, + }, + }; + let database = crate::KbDatabase::connect_and_initialize(&config) + .await + .expect("database init must succeed"); + let dto = crate::KbDbRuntimeEventDto::new( + "http_request".to_string(), + crate::KbDbRuntimeEventLevel::Info, + "demo_http".to_string(), + "getHealth executed".to_string(), + ); + let inserted_id = crate::insert_db_runtime_event(&database, &dto) + .await + .expect("insert must succeed"); + assert!(inserted_id > 0); + let listed = crate::list_recent_db_runtime_events(&database, 10) + .await + .expect("list must succeed"); + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].event_kind, "http_request"); + assert_eq!(listed[0].level, crate::KbDbRuntimeEventLevel::Info); + } +} diff --git a/kb_lib/src/db/queries/known_http_endpoint.rs b/kb_lib/src/db/queries/known_http_endpoint.rs new file mode 100644 index 0000000..8e25cbc --- /dev/null +++ b/kb_lib/src/db/queries/known_http_endpoint.rs @@ -0,0 +1,195 @@ +// file: kb_lib/src/db/queries/known_http_endpoint.rs + +//! Queries for `kb_known_http_endpoints`. + +/// Inserts or updates one known HTTP endpoint row. +pub async fn upsert_known_http_endpoint( + database: &crate::KbDatabase, + dto: &crate::KbKnownHttpEndpointDto, +) -> Result<(), crate::KbError> { + let entity_result = crate::KbKnownHttpEndpointEntity::try_from(dto.clone()); + let entity = match entity_result { + Ok(entity) => entity, + Err(error) => return Err(error), + }; + + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_known_http_endpoints ( + name, + provider, + url, + enabled, + roles_json, + last_seen_at, + updated_at +) +VALUES (?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(name) DO UPDATE SET + provider = excluded.provider, + url = excluded.url, + enabled = excluded.enabled, + roles_json = excluded.roles_json, + last_seen_at = excluded.last_seen_at, + updated_at = excluded.updated_at + "#, + ) + .bind(entity.name) + .bind(entity.provider) + .bind(entity.url) + .bind(entity.enabled) + .bind(entity.roles_json) + .bind(entity.last_seen_at) + .bind(entity.updated_at) + .execute(pool) + .await; + match query_result { + Ok(_) => Ok(()), + Err(error) => Err(crate::KbError::Db(format!( + "cannot upsert kb_known_http_endpoints on sqlite: {}", + error + ))), + } + } + } +} + +/// Reads one known HTTP endpoint by name. +pub async fn get_known_http_endpoint( + database: &crate::KbDatabase, + name: &str, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query_as::( + r#" +SELECT + name, + provider, + url, + enabled, + roles_json, + last_seen_at, + updated_at +FROM kb_known_http_endpoints +WHERE name = ? +LIMIT 1 + "#, + ) + .bind(name) + .fetch_optional(pool) + .await; + let entity_option = match query_result { + Ok(entity_option) => entity_option, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot read known http endpoint '{}' on sqlite: {}", + name, error + ))); + } + }; + match entity_option { + Some(entity) => { + let dto_result = crate::KbKnownHttpEndpointDto::try_from(entity); + match dto_result { + Ok(dto) => Ok(Some(dto)), + Err(error) => Err(error), + } + } + None => Ok(None), + } + } + } +} + +/// Lists all known HTTP endpoints. +pub async fn list_known_http_endpoints( + database: &crate::KbDatabase, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query_as::( + r#" +SELECT + name, + provider, + url, + enabled, + roles_json, + last_seen_at, + updated_at +FROM kb_known_http_endpoints +ORDER BY name ASC + "#, + ) + .fetch_all(pool) + .await; + let entities = match query_result { + Ok(entities) => entities, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot list known http endpoints on sqlite: {}", + error + ))); + } + }; + let mut dtos = std::vec::Vec::new(); + for entity in entities { + let dto_result = crate::KbKnownHttpEndpointDto::try_from(entity); + let dto = match dto_result { + Ok(dto) => dto, + Err(error) => return Err(error), + }; + dtos.push(dto); + } + Ok(dtos) + } + } +} + +#[cfg(test)] +mod tests { + #[tokio::test] + async fn known_http_endpoint_roundtrip_works() { + let tempdir = tempfile::tempdir().expect("tempdir must succeed"); + let database_path = tempdir.path().join("known_http_endpoint.sqlite3"); + let config = crate::KbDatabaseConfig { + enabled: true, + backend: crate::KbDatabaseBackend::Sqlite, + sqlite: crate::KbSqliteDatabaseConfig { + path: database_path.to_string_lossy().to_string(), + create_if_missing: true, + busy_timeout_ms: 5000, + max_connections: 1, + auto_initialize_schema: true, + use_wal: true, + }, + }; + let database = crate::KbDatabase::connect_and_initialize(&config) + .await + .expect("database init must succeed"); + let dto = crate::KbKnownHttpEndpointDto::new( + "helius_primary_http".to_string(), + "helius".to_string(), + "https://mainnet.helius-rpc.com".to_string(), + true, + vec!["http_queries".to_string(), "http_transactions".to_string()], + ); + crate::upsert_known_http_endpoint(&database, &dto) + .await + .expect("upsert must succeed"); + let fetched = crate::get_known_http_endpoint(&database, "helius_primary_http") + .await + .expect("fetch must succeed"); + assert!(fetched.is_some()); + let fetched = fetched.expect("endpoint must exist"); + assert_eq!(fetched.provider, "helius"); + assert_eq!(fetched.roles.len(), 2); + let listed = crate::list_known_http_endpoints(&database) + .await + .expect("list must succeed"); + assert_eq!(listed.len(), 1); + } +} diff --git a/kb_lib/src/db/queries/known_ws_endpoint.rs b/kb_lib/src/db/queries/known_ws_endpoint.rs new file mode 100644 index 0000000..31e22cd --- /dev/null +++ b/kb_lib/src/db/queries/known_ws_endpoint.rs @@ -0,0 +1,195 @@ +// file: kb_lib/src/db/queries/known_ws_endpoint.rs + +//! Queries for `kb_known_ws_endpoints`. + +/// Inserts or updates one known WS endpoint row. +pub async fn upsert_known_ws_endpoint( + database: &crate::KbDatabase, + dto: &crate::KbKnownWsEndpointDto, +) -> Result<(), crate::KbError> { + let entity_result = crate::KbKnownWsEndpointEntity::try_from(dto.clone()); + let entity = match entity_result { + Ok(entity) => entity, + Err(error) => return Err(error), + }; + + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_known_ws_endpoints ( + name, + provider, + url, + enabled, + roles_json, + last_seen_at, + updated_at +) +VALUES (?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(name) DO UPDATE SET + provider = excluded.provider, + url = excluded.url, + enabled = excluded.enabled, + roles_json = excluded.roles_json, + last_seen_at = excluded.last_seen_at, + updated_at = excluded.updated_at + "#, + ) + .bind(entity.name) + .bind(entity.provider) + .bind(entity.url) + .bind(entity.enabled) + .bind(entity.roles_json) + .bind(entity.last_seen_at) + .bind(entity.updated_at) + .execute(pool) + .await; + match query_result { + Ok(_) => Ok(()), + Err(error) => Err(crate::KbError::Db(format!( + "cannot upsert kb_known_ws_endpoints on sqlite: {}", + error + ))), + } + } + } +} + +/// Reads one known WS endpoint by name. +pub async fn get_known_ws_endpoint( + database: &crate::KbDatabase, + name: &str, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query_as::( + r#" +SELECT + name, + provider, + url, + enabled, + roles_json, + last_seen_at, + updated_at +FROM kb_known_ws_endpoints +WHERE name = ? +LIMIT 1 + "#, + ) + .bind(name) + .fetch_optional(pool) + .await; + let entity_option = match query_result { + Ok(entity_option) => entity_option, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot read known ws endpoint '{}' on sqlite: {}", + name, error + ))); + } + }; + match entity_option { + Some(entity) => { + let dto_result = crate::KbKnownWsEndpointDto::try_from(entity); + match dto_result { + Ok(dto) => Ok(Some(dto)), + Err(error) => Err(error), + } + } + None => Ok(None), + } + } + } +} + +/// Lists all known WS endpoints. +pub async fn list_known_ws_endpoints( + database: &crate::KbDatabase, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query_as::( + r#" +SELECT + name, + provider, + url, + enabled, + roles_json, + last_seen_at, + updated_at +FROM kb_known_ws_endpoints +ORDER BY name ASC + "#, + ) + .fetch_all(pool) + .await; + let entities = match query_result { + Ok(entities) => entities, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot list known ws endpoints on sqlite: {}", + error + ))); + } + }; + let mut dtos = std::vec::Vec::new(); + for entity in entities { + let dto_result = crate::KbKnownWsEndpointDto::try_from(entity); + let dto = match dto_result { + Ok(dto) => dto, + Err(error) => return Err(error), + }; + dtos.push(dto); + } + Ok(dtos) + } + } +} + +#[cfg(test)] +mod tests { + #[tokio::test] + async fn known_ws_endpoint_roundtrip_works() { + let tempdir = tempfile::tempdir().expect("tempdir must succeed"); + let database_path = tempdir.path().join("known_ws_endpoint.sqlite3"); + let config = crate::KbDatabaseConfig { + enabled: true, + backend: crate::KbDatabaseBackend::Sqlite, + sqlite: crate::KbSqliteDatabaseConfig { + path: database_path.to_string_lossy().to_string(), + create_if_missing: true, + busy_timeout_ms: 5000, + max_connections: 1, + auto_initialize_schema: true, + use_wal: true, + }, + }; + let database = crate::KbDatabase::connect_and_initialize(&config) + .await + .expect("database init must succeed"); + let dto = crate::KbKnownWsEndpointDto::new( + "mainnet_public_ws_slots".to_string(), + "solana".to_string(), + "wss://api.mainnet.solana.com".to_string(), + true, + vec!["ws_slots".to_string(), "ws_subscriptions".to_string()], + ); + crate::upsert_known_ws_endpoint(&database, &dto) + .await + .expect("upsert must succeed"); + let fetched = crate::get_known_ws_endpoint(&database, "mainnet_public_ws_slots") + .await + .expect("fetch must succeed"); + assert!(fetched.is_some()); + let fetched = fetched.expect("endpoint must exist"); + assert_eq!(fetched.provider, "solana"); + assert_eq!(fetched.roles.len(), 2); + let listed = crate::list_known_ws_endpoints(&database) + .await + .expect("list must succeed"); + assert_eq!(listed.len(), 1); + } +} diff --git a/kb_lib/src/db/schema.rs b/kb_lib/src/db/schema.rs index 763e553..ee27d3c 100644 --- a/kb_lib/src/db/schema.rs +++ b/kb_lib/src/db/schema.rs @@ -3,9 +3,7 @@ //! Database schema initialization. /// Ensures that the database schema exists. -pub(super) async fn ensure_schema( - database: &crate::KbDatabase, -) -> Result<(), crate::KbError> { +pub(crate) async fn ensure_schema(database: &crate::KbDatabase) -> Result<(), crate::KbError> { match database.connection() { crate::KbDatabaseConnection::Sqlite(pool) => { let metadata_table_result = sqlx::query( @@ -25,6 +23,82 @@ CREATE TABLE IF NOT EXISTS kb_db_metadata ( error ))); } + let known_http_endpoints_result = sqlx::query( + r#" +CREATE TABLE IF NOT EXISTS kb_known_http_endpoints ( + name TEXT NOT NULL PRIMARY KEY, + provider TEXT NOT NULL, + url TEXT NOT NULL, + enabled INTEGER NOT NULL, + roles_json TEXT NOT NULL, + last_seen_at TEXT NULL, + updated_at TEXT NOT NULL +) + "#, + ) + .execute(pool) + .await; + if let Err(error) = known_http_endpoints_result { + return Err(crate::KbError::Db(format!( + "cannot create table kb_known_http_endpoints on sqlite: {}", + error + ))); + } + let known_ws_endpoints_result = sqlx::query( + r#" +CREATE TABLE IF NOT EXISTS kb_known_ws_endpoints ( + name TEXT NOT NULL PRIMARY KEY, + provider TEXT NOT NULL, + url TEXT NOT NULL, + enabled INTEGER NOT NULL, + roles_json TEXT NOT NULL, + last_seen_at TEXT NULL, + updated_at TEXT NOT NULL +) + "#, + ) + .execute(pool) + .await; + if let Err(error) = known_ws_endpoints_result { + return Err(crate::KbError::Db(format!( + "cannot create table kb_known_ws_endpoints on sqlite: {}", + error + ))); + } + let runtime_events_result = sqlx::query( + r#" +CREATE TABLE IF NOT EXISTS kb_db_runtime_events ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + event_kind TEXT NOT NULL, + level INTEGER NOT NULL, + source TEXT NOT NULL, + message TEXT NOT NULL, + created_at TEXT NOT NULL +) + "#, + ) + .execute(pool) + .await; + if let Err(error) = runtime_events_result { + return Err(crate::KbError::Db(format!( + "cannot create table kb_db_runtime_events on sqlite: {}", + error + ))); + } + let runtime_events_index_result = sqlx::query( + r#" +CREATE INDEX IF NOT EXISTS kb_idx_db_runtime_events_created_at +ON kb_db_runtime_events (created_at) + "#, + ) + .execute(pool) + .await; + if let Err(error) = runtime_events_index_result { + return Err(crate::KbError::Db(format!( + "cannot create index kb_idx_db_runtime_events_created_at on sqlite: {}", + error + ))); + } let schema_version = crate::KbDbMetadataDto::new( "schema_version".to_string(), env!("CARGO_PKG_VERSION").to_string(), @@ -34,6 +108,6 @@ CREATE TABLE IF NOT EXISTS kb_db_metadata ( return Err(error); } Ok(()) - }, + } } } diff --git a/kb_lib/src/db/types.rs b/kb_lib/src/db/types.rs index d90e326..ef2569b 100644 --- a/kb_lib/src/db/types.rs +++ b/kb_lib/src/db/types.rs @@ -3,5 +3,7 @@ //! Database shared types. mod database_backend; +mod runtime_event_level; pub use crate::db::types::database_backend::KbDatabaseBackend; +pub use crate::db::types::runtime_event_level::KbDbRuntimeEventLevel; diff --git a/kb_lib/src/db/types/runtime_event_level.rs b/kb_lib/src/db/types/runtime_event_level.rs new file mode 100644 index 0000000..f72aa85 --- /dev/null +++ b/kb_lib/src/db/types/runtime_event_level.rs @@ -0,0 +1,46 @@ +// file: kb_lib/src/db/types/runtime_event_level.rs + +//! Runtime event severity level. + +/// Runtime event level used by the local database layer. +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum KbDbRuntimeEventLevel { + /// Diagnostic trace event. + Trace, + /// Diagnostic debug event. + Debug, + /// Informational event. + Info, + /// Warning event. + Warn, + /// Error event. + Error, +} + +impl KbDbRuntimeEventLevel { + /// Converts the level to its stable integer representation. + pub fn to_i16(self) -> i16 { + match self { + Self::Trace => 0, + Self::Debug => 1, + Self::Info => 2, + Self::Warn => 3, + Self::Error => 4, + } + } + + /// Restores a level from its stable integer representation. + pub fn from_i16(value: i16) -> Result { + match value { + 0 => Ok(Self::Trace), + 1 => Ok(Self::Debug), + 2 => Ok(Self::Info), + 3 => Ok(Self::Warn), + 4 => Ok(Self::Error), + _ => Err(crate::KbError::Db(format!( + "invalid KbDbRuntimeEventLevel value: {}", + value + ))), + } + } +} diff --git a/kb_lib/src/lib.rs b/kb_lib/src/lib.rs index 8ce7ed2..0c4a740 100644 --- a/kb_lib/src/lib.rs +++ b/kb_lib/src/lib.rs @@ -71,4 +71,19 @@ pub use crate::db::KbDbMetadataEntity; pub use crate::db::get_db_metadata; pub use crate::db::list_db_metadata; pub use crate::db::upsert_db_metadata; +pub use crate::db::KbDbRuntimeEventDto; +pub use crate::db::KbDbRuntimeEventEntity; +pub use crate::db::KbDbRuntimeEventLevel; +pub use crate::db::KbKnownHttpEndpointDto; +pub use crate::db::KbKnownHttpEndpointEntity; +pub use crate::db::KbKnownWsEndpointDto; +pub use crate::db::KbKnownWsEndpointEntity; +pub use crate::db::get_known_http_endpoint; +pub use crate::db::get_known_ws_endpoint; +pub use crate::db::insert_db_runtime_event; +pub use crate::db::list_known_http_endpoints; +pub use crate::db::list_known_ws_endpoints; +pub use crate::db::list_recent_db_runtime_events; +pub use crate::db::upsert_known_http_endpoint; +pub use crate::db::upsert_known_ws_endpoint;