This commit is contained in:
2026-04-23 09:22:11 +02:00
parent c36d6b9ded
commit 3b8e029cde
20 changed files with 1123 additions and 16 deletions

View File

@@ -17,3 +17,4 @@
0.4.3 - Pool dendpoints 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

View File

@@ -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"

View File

@@ -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 à lanalyse,
- distinguer événements runtime, observations on-chain et événements métier.
#### 0.5.x — Consolidation de la couche stockage
À faire :
- conserver labstraction 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 lorganisation 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 louverture 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 labstraction du backend dès cette phase SQLite,
6. préparer ensuite le stockage des tokens observés et des premiers signaux persistés.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<i64>,
/// 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<chrono::Utc>,
}
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<crate::KbDbRuntimeEventEntity> for KbDbRuntimeEventDto {
type Error = crate::KbError;
fn try_from(entity: crate::KbDbRuntimeEventEntity) -> Result<Self, Self::Error> {
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,
})
}
}

View File

@@ -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<std::string::String>,
/// Optional last seen timestamp.
pub last_seen_at: std::option::Option<chrono::DateTime<chrono::Utc>>,
/// Last update timestamp.
pub updated_at: chrono::DateTime<chrono::Utc>,
}
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<std::string::String>,
) -> Self {
Self {
name,
provider,
url,
enabled,
roles,
last_seen_at: None,
updated_at: chrono::Utc::now(),
}
}
}
impl TryFrom<crate::KbKnownHttpEndpointEntity> for KbKnownHttpEndpointDto {
type Error = crate::KbError;
fn try_from(entity: crate::KbKnownHttpEndpointEntity) -> Result<Self, Self::Error> {
let roles_result =
serde_json::from_str::<std::vec::Vec<std::string::String>>(&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<KbKnownHttpEndpointDto> for crate::KbKnownHttpEndpointEntity {
type Error = crate::KbError;
fn try_from(dto: KbKnownHttpEndpointDto) -> Result<Self, Self::Error> {
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(),
})
}
}

View File

@@ -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<std::string::String>,
/// Optional last seen timestamp.
pub last_seen_at: std::option::Option<chrono::DateTime<chrono::Utc>>,
/// Last update timestamp.
pub updated_at: chrono::DateTime<chrono::Utc>,
}
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<std::string::String>,
) -> Self {
Self {
name,
provider,
url,
enabled,
roles,
last_seen_at: None,
updated_at: chrono::Utc::now(),
}
}
}
impl TryFrom<crate::KbKnownWsEndpointEntity> for KbKnownWsEndpointDto {
type Error = crate::KbError;
fn try_from(entity: crate::KbKnownWsEndpointEntity) -> Result<Self, Self::Error> {
let roles_result =
serde_json::from_str::<std::vec::Vec<std::string::String>>(&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<KbKnownWsEndpointDto> for crate::KbKnownWsEndpointEntity {
type Error = crate::KbError;
fn try_from(dto: KbKnownWsEndpointDto) -> Result<Self, Self::Error> {
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(),
})
}
}

View File

@@ -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;

View File

@@ -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,
}

View File

@@ -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<std::string::String>,
/// Last update timestamp encoded as RFC3339 UTC text.
pub updated_at: std::string::String,
}

View File

@@ -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<std::string::String>,
/// Last update timestamp encoded as RFC3339 UTC text.
pub updated_at: std::string::String,
}

View File

@@ -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;

View File

@@ -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<i64, crate::KbError> {
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<std::vec::Vec<crate::KbDbRuntimeEventDto>, crate::KbError> {
if limit == 0 {
return Ok(std::vec::Vec::new());
}
match database.connection() {
crate::KbDatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::KbDbRuntimeEventEntity>(
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);
}
}

View File

@@ -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<std::option::Option<crate::KbKnownHttpEndpointDto>, crate::KbError> {
match database.connection() {
crate::KbDatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::KbKnownHttpEndpointEntity>(
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<std::vec::Vec<crate::KbKnownHttpEndpointDto>, crate::KbError> {
match database.connection() {
crate::KbDatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::KbKnownHttpEndpointEntity>(
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);
}
}

View File

@@ -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<std::option::Option<crate::KbKnownWsEndpointDto>, crate::KbError> {
match database.connection() {
crate::KbDatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::KbKnownWsEndpointEntity>(
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<std::vec::Vec<crate::KbKnownWsEndpointDto>, crate::KbError> {
match database.connection() {
crate::KbDatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::KbKnownWsEndpointEntity>(
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);
}
}

View File

@@ -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(())
},
}
}
}

View File

@@ -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;

View File

@@ -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<Self, crate::KbError> {
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
))),
}
}
}

View File

@@ -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;