lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit ccb17e40486c5ac568e391fad959ac517a6a4879
parent 2e65f6dd94d6e7e9b5bc78fe83efaf320af62179
Author: triesap <tyson@radroots.org>
Date:   Thu, 13 Nov 2025 02:02:02 +0000

workspace: add `radroots-sql-core` migrations and `tangle-schema` result contracts

Diffstat:
MCargo.lock | 1+
Msql-core/src/lib.rs | 1+
Asql-core/src/migrations.rs | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtangle-schema/Cargo.toml | 1+
Mtangle-schema/bindings/ts/package.json | 3++-
Mtangle-schema/bindings/ts/src/types.ts | 22++++++++++++++++++++++
Mtangle-schema/src/tables/log_error.rs | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtangle-sql-wasm/src/lib.rs | 34+++++++++++++++++++++++-----------
Atangle-sql/migrations/0000_init.down.sql | 2++
Atangle-sql/migrations/0000_init.up.sql | 5+++++
Atangle-sql/migrations/0001_log_error.down.sql | 2++
Atangle-sql/migrations/0001_log_error.up.sql | 14++++++++++++++
Mtangle-sql/src/lib.rs | 9+++++++++
Atangle-sql/src/migrations.rs | 30++++++++++++++++++++++++++++++
14 files changed, 331 insertions(+), 12 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1856,6 +1856,7 @@ dependencies = [ name = "radroots-tangle-schema" version = "0.1.0" dependencies = [ + "radroots-types", "serde", "serde_json", "ts-rs", diff --git a/sql-core/src/lib.rs b/sql-core/src/lib.rs @@ -4,6 +4,7 @@ extern crate alloc; pub mod error; +pub mod migrations; #[cfg(all(feature = "web", target_arch = "wasm32"))] mod executor_wasm; diff --git a/sql-core/src/migrations.rs b/sql-core/src/migrations.rs @@ -0,0 +1,91 @@ +use crate::SqlExecutor; +use crate::error::SqlError; +use serde_json::{Value, json}; + +#[derive(Clone, Copy, Debug)] +pub struct Migration { + pub name: &'static str, + pub up_sql: &'static str, + pub down_sql: &'static str, +} + +pub fn migrations_run_all_up<E>(executor: &E, migrations: &[Migration]) -> Result<(), SqlError> +where + E: SqlExecutor, +{ + ensure_table(executor)?; + for migration in migrations { + if !is_applied(executor, migration.name)? { + executor.begin()?; + let result = (|| -> Result<(), SqlError> { + let _ = executor.exec(migration.up_sql, "[]")?; + mark_applied(executor, migration.name)?; + Ok(()) + })(); + match result { + Ok(()) => { + executor.commit()?; + } + Err(err) => { + let _ = executor.rollback(); + return Err(err); + } + } + } + } + Ok(()) +} + +pub fn migrations_run_all_down<E>(executor: &E, migrations: &[Migration]) -> Result<(), SqlError> +where + E: SqlExecutor, +{ + ensure_table(executor)?; + executor.begin()?; + for migration in migrations.iter().rev() { + let params = json!([migration.name]).to_string(); + let _ = executor.exec("delete from __migrations where name = ?", &params)?; + let _ = executor.exec(migration.down_sql, "[]")?; + } + executor.commit()?; + Ok(()) +} + +fn ensure_table<E>(executor: &E) -> Result<(), SqlError> +where + E: SqlExecutor, +{ + let _ = executor.exec( + "create table if not exists __migrations(id integer primary key, name text not null unique, applied_at text not null default (datetime('now')))", + "[]", + )?; + Ok(()) +} + +fn is_applied<E>(executor: &E, name: &str) -> Result<bool, SqlError> +where + E: SqlExecutor, +{ + let params = json!([name]).to_string(); + let json = executor.query_raw( + "select 1 as applied from __migrations where name = ? limit 1", + &params, + )?; + if json.trim().is_empty() { + return Ok(false); + } + let rows: Vec<Value> = serde_json::from_str(&json)?; + Ok(!rows.is_empty()) +} + +fn mark_applied<E>(executor: &E, name: &str) -> Result<(), SqlError> +where + E: SqlExecutor, +{ + let params = json!([name]).to_string(); + let _ = executor.exec( + "insert or ignore into __migrations(name) values(?)", + &params, + )?; + Ok(()) +} diff --git a/tangle-schema/Cargo.toml b/tangle-schema/Cargo.toml @@ -18,3 +18,4 @@ ts-rs = ["dep:ts-rs"] serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } ts-rs = { workspace = true, optional = true } +radroots-types = { workspace = true } diff --git a/tangle-schema/bindings/ts/package.json b/tangle-schema/bindings/ts/package.json @@ -22,7 +22,8 @@ "build:esm": "tsc -p tsconfig.esm.json", "build:cjs": "tsc -p tsconfig.cjs.json", "build": "npm run build:esm && npm run build:cjs", - "prebuild": "npm run clean", + "prebuild": "npm run clean && npm run prepend-imports", + "prepend-imports": "bash -c 'f=./src/types.ts; line=\"import type { IResult, IResultList } from \\\"@radroots/types-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")'", "clean": "rimraf dist", "dev": "npm run watch", "watch": "tsc -w" diff --git a/tangle-schema/bindings/ts/src/types.ts b/tangle-schema/bindings/ts/src/types.ts @@ -1,11 +1,33 @@ +import type { IResult, IResultList } from "@radroots/types-bindings"; + // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +export type ILogErrorCreate = ILogErrorFields; + +export type ILogErrorCreateResolve = IResult<string>; + +export type ILogErrorDelete = ILogErrorFindOne; + +export type ILogErrorDeleteResolve = IResult<string>; + export type ILogErrorFields = { error: string, message: string, stack_trace: string | null, cause: string | null, app_system: string, app_version: string, nostr_pubkey: string, data: string | null, }; export type ILogErrorFieldsFilter = { id?: string, created_at?: string, updated_at?: string, error?: string, message?: string, stack_trace?: string, cause?: string, app_system?: string, app_version?: string, nostr_pubkey?: string, data?: string, }; export type ILogErrorFieldsPartial = { error?: string | null, message?: string | null, stack_trace?: string | null, cause?: string | null, app_system?: string | null, app_version?: string | null, nostr_pubkey?: string | null, data?: string | null, }; +export type ILogErrorFindMany = { filter: ILogErrorFieldsFilter | null, }; + +export type ILogErrorFindManyResolve = IResultList<LogError>; + +export type ILogErrorFindOne = { on: LogErrorQueryBindValues, }; + +export type ILogErrorFindOneResolve = IResult<LogError>; + +export type ILogErrorUpdate = { on: LogErrorQueryBindValues, fields: ILogErrorFieldsPartial, }; + +export type ILogErrorUpdateResolve = IResult<string>; + export type LogError = { id: string, created_at: string, updated_at: string, error: string, message: string, stack_trace: string | null, cause: string | null, app_system: string, app_version: string, nostr_pubkey: string, data: string | null, }; export type LogErrorQueryBindValues = { id: string, } | { nostr_pubkey: string, }; diff --git a/tangle-schema/src/tables/log_error.rs b/tangle-schema/src/tables/log_error.rs @@ -1,3 +1,4 @@ +use radroots_types::types::{IResult, IResultList}; use serde::{Deserialize, Serialize}; #[cfg(feature = "ts-rs")] use ts_rs::TS; @@ -91,3 +92,130 @@ pub enum LogErrorQueryBindValues { Id { id: String }, NostrPubkey { nostr_pubkey: String }, } + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr( + feature = "ts-rs", + ts( + export, + export_to = "types.ts", + rename = "ILogErrorCreate", + type = "ILogErrorFields" + ) +)] + +pub struct ILogErrorCreateTs; +pub type ILogErrorCreate = ILogErrorFields; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr( + feature = "ts-rs", + ts( + export, + export_to = "types.ts", + rename = "ILogErrorCreateResolve", + type = "IResult<string>" + ) +)] +pub struct ILogErrorCreateResolveTs; +pub type ILogErrorCreateResolve = IResult<String>; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr( + feature = "ts-rs", + ts(export, export_to = "types.ts", rename = "ILogErrorFindOne") +)] +#[derive(Deserialize, Serialize)] +pub struct ILogErrorFindOneArgs { + pub on: LogErrorQueryBindValues, +} +pub type ILogErrorFindOne = ILogErrorFindOneArgs; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr( + feature = "ts-rs", + ts( + export, + export_to = "types.ts", + rename = "ILogErrorFindOneResolve", + type = "IResult<LogError>" + ) +)] +pub struct ILogErrorFindOneResolveTs; +pub type ILogErrorFindOneResolve = IResult<LogError>; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr( + feature = "ts-rs", + ts(export, export_to = "types.ts", rename = "ILogErrorFindMany") +)] +#[derive(Deserialize, Serialize)] +pub struct ILogErrorFindManyArgs { + pub filter: Option<ILogErrorFieldsFilter>, +} +pub type ILogErrorFindMany = ILogErrorFindManyArgs; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr( + feature = "ts-rs", + ts( + export, + export_to = "types.ts", + rename = "ILogErrorFindManyResolve", + type = "IResultList<LogError>" + ) +)] +pub struct ILogErrorFindManyResolveTs; +pub type ILogErrorFindManyResolve = IResultList<LogError>; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr( + feature = "ts-rs", + ts( + export, + export_to = "types.ts", + rename = "ILogErrorDelete", + type = "ILogErrorFindOne" + ) +)] + +pub struct ILogErrorDeleteTs; +pub type ILogErrorDelete = ILogErrorFields; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr( + feature = "ts-rs", + ts( + export, + export_to = "types.ts", + rename = "ILogErrorDeleteResolve", + type = "IResult<string>" + ) +)] +pub struct ILogErrorDeleteResolveTs; +pub type ILogErrorDeleteResolve = IResult<String>; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr( + feature = "ts-rs", + ts(export, export_to = "types.ts", rename = "ILogErrorUpdate") +)] +#[derive(Deserialize, Serialize)] +pub struct ILogErrorUpdateArgs { + pub on: LogErrorQueryBindValues, + pub fields: ILogErrorFieldsPartial, +} +pub type ILogErrorUpdate = ILogErrorUpdateArgs; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr( + feature = "ts-rs", + ts( + export, + export_to = "types.ts", + rename = "ILogErrorUpdateResolve", + type = "IResult<string>" + ) +)] +pub struct ILogErrorUpdateResolveTs; +pub type ILogErrorUpdateResolve = IResult<String>; diff --git a/tangle-sql-wasm/src/lib.rs b/tangle-sql-wasm/src/lib.rs @@ -7,13 +7,25 @@ use radroots_tangle_schema::log_error::{ ILogErrorFields, ILogErrorFieldsFilter, ILogErrorFieldsPartial, LogError, LogErrorQueryBindValues, }; -use radroots_tangle_sql::log_error; +use radroots_tangle_sql::{log_error, migrations}; pub mod utils; pub use utils::*; -#[wasm_bindgen(js_name = tangle_log_error_create)] -pub fn tangle_log_error_create(opts_json: &str) -> Result<JsValue, JsValue> { +#[wasm_bindgen(js_name = tangle_db_run_migrations)] +pub fn tangle_db_run_migrations() -> Result<(), JsValue> { + let exec = WasmSqlExecutor::new(); + migrations::run_all_up(&exec).map_err(radroots_sql_wasm_core::err_js) +} + +#[wasm_bindgen(js_name = tangle_db_reset_database)] +pub fn tangle_db_reset_database() -> Result<(), JsValue> { + let exec = WasmSqlExecutor::new(); + migrations::run_all_down(&exec).map_err(radroots_sql_wasm_core::err_js) +} + +#[wasm_bindgen(js_name = tangle_db_log_error_create)] +pub fn tangle_db_log_error_create(opts_json: &str) -> Result<JsValue, JsValue> { let payload = radroots_sql_wasm_core::parse_json::<ILogErrorFields>(opts_json) .map_err(radroots_sql_wasm_core::err_js)?; let exec = WasmSqlExecutor::new(); @@ -21,8 +33,8 @@ pub fn tangle_log_error_create(opts_json: &str) -> Result<JsValue, JsValue> { value_to_js(out) } -#[wasm_bindgen(js_name = tangle_log_error_find_many)] -pub fn tangle_log_error_find_many(filter_json: &str) -> Result<JsValue, JsValue> { +#[wasm_bindgen(js_name = tangle_db_log_error_find_many)] +pub fn tangle_db_log_error_find_many(filter_json: &str) -> Result<JsValue, JsValue> { let filter = parse_optional_json::<ILogErrorFieldsFilter>(filter_json) .map_err(radroots_sql_wasm_core::err_js)?; let exec = WasmSqlExecutor::new(); @@ -31,8 +43,8 @@ pub fn tangle_log_error_find_many(filter_json: &str) -> Result<JsValue, JsValue> value_to_js(out) } -#[wasm_bindgen(js_name = tangle_log_error_find_one)] -pub fn tangle_log_error_find_one(bind_json: &str) -> Result<JsValue, JsValue> { +#[wasm_bindgen(js_name = tangle_db_log_error_find_one)] +pub fn tangle_db_log_error_find_one(bind_json: &str) -> Result<JsValue, JsValue> { let bind = radroots_sql_wasm_core::parse_json::<LogErrorQueryBindValues>(bind_json) .map_err(radroots_sql_wasm_core::err_js)?; let exec = WasmSqlExecutor::new(); @@ -41,8 +53,8 @@ pub fn tangle_log_error_find_one(bind_json: &str) -> Result<JsValue, JsValue> { value_to_js(out) } -#[wasm_bindgen(js_name = tangle_log_error_update)] -pub fn tangle_log_error_update(id: &str, fields_json: &str) -> Result<JsValue, JsValue> { +#[wasm_bindgen(js_name = tangle_db_log_error_update)] +pub fn tangle_db_log_error_update(id: &str, fields_json: &str) -> Result<JsValue, JsValue> { let fields = radroots_sql_wasm_core::parse_json::<ILogErrorFieldsPartial>(fields_json) .map_err(radroots_sql_wasm_core::err_js)?; let exec = WasmSqlExecutor::new(); @@ -50,8 +62,8 @@ pub fn tangle_log_error_update(id: &str, fields_json: &str) -> Result<JsValue, J outcome_to_js(outcome) } -#[wasm_bindgen(js_name = tangle_log_error_delete)] -pub fn tangle_log_error_delete(bind_json: &str) -> Result<JsValue, JsValue> { +#[wasm_bindgen(js_name = tangle_db_log_error_delete)] +pub fn tangle_db_log_error_delete(bind_json: &str) -> Result<JsValue, JsValue> { let bind = radroots_sql_wasm_core::parse_json::<LogErrorQueryBindValues>(bind_json) .map_err(radroots_sql_wasm_core::err_js)?; let exec = WasmSqlExecutor::new(); diff --git a/tangle-sql/migrations/0000_init.down.sql b/tangle-sql/migrations/0000_init.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS __migrations; +\ No newline at end of file diff --git a/tangle-sql/migrations/0000_init.up.sql b/tangle-sql/migrations/0000_init.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS __migrations ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) +); diff --git a/tangle-sql/migrations/0001_log_error.down.sql b/tangle-sql/migrations/0001_log_error.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS log_error; +\ No newline at end of file diff --git a/tangle-sql/migrations/0001_log_error.up.sql b/tangle-sql/migrations/0001_log_error.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS log_error ( + id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), + created_at DATETIME NOT NULL CHECK(length(created_at) = 24), + updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), + error TEXT NOT NULL, + message TEXT NOT NULL, + stack_trace TEXT, + cause TEXT, + app_system TEXT NOT NULL, + app_version TEXT NOT NULL, + nostr_pubkey TEXT NOT NULL, + data TEXT +); +\ No newline at end of file diff --git a/tangle-sql/src/lib.rs b/tangle-sql/src/lib.rs @@ -1,6 +1,7 @@ pub use radroots_sql_core::error::SqlError; pub use radroots_sql_core::{ExecOutcome, SqlExecutor}; +pub mod migrations; pub mod tables; pub use tables::log_error; @@ -17,6 +18,14 @@ impl<E: SqlExecutor> TangleSql<E> { &self.executor } + pub fn migrate_up(&self) -> Result<(), SqlError> { + crate::migrations::run_all_up(self.executor()) + } + + pub fn migrate_down(&self) -> Result<(), SqlError> { + crate::migrations::run_all_down(self.executor()) + } + pub fn insert_log_error( &self, fields: radroots_tangle_schema::log_error::ILogErrorFields, diff --git a/tangle-sql/src/migrations.rs b/tangle-sql/src/migrations.rs @@ -0,0 +1,30 @@ +use radroots_sql_core::SqlExecutor; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::migrations::{Migration, migrations_run_all_down, migrations_run_all_up}; + +pub static MIGRATIONS: &[Migration] = &[ + Migration { + name: "0000_init", + up_sql: include_str!("../migrations/0000_init.up.sql"), + down_sql: include_str!("../migrations/0000_init.down.sql"), + }, + Migration { + name: "0001_log_error", + up_sql: include_str!("../migrations/0001_log_error.up.sql"), + down_sql: include_str!("../migrations/0001_log_error.down.sql"), + }, +]; + +pub fn run_all_up<E>(executor: &E) -> Result<(), SqlError> +where + E: SqlExecutor, +{ + migrations_run_all_up(executor, MIGRATIONS) +} + +pub fn run_all_down<E>(executor: &E) -> Result<(), SqlError> +where + E: SqlExecutor, +{ + migrations_run_all_down(executor, MIGRATIONS) +}