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:
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 = ?", ¶ms)?;
+ 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",
+ ¶ms,
+ )?;
+ 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(?)",
+ ¶ms,
+ )?;
+ 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)
+}