diff --git a/Cargo.lock b/Cargo.lock index 5da9138..cf98cbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,6 +140,16 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "authorization" +version = "0.1.0" +dependencies = [ + "heromodels", + "heromodels_core", + "rhai", + "serde", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -2255,9 +2265,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rhai" -version = "1.22.2" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2780e813b755850e50b178931aaf94ed24f6817f46aaaf5d21c13c12d939a249" +checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" dependencies = [ "ahash", "bitflags 2.9.1", @@ -2364,6 +2374,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "rhailib_dsl" +version = "0.1.0" +dependencies = [ + "authorization", + "chrono", + "heromodels", + "heromodels-derive", + "heromodels_core", + "rhai", + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "rhailib_worker" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 01c9f33..b7f06ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,6 @@ members = [ "src/monitor", # Added the new monitor package to workspace "src/repl", # Added the refactored REPL package "examples", - "src/rhai_engine_ui", + "src/rhai_engine_ui", "src/authorization", "src/dsl", ] resolver = "2" # Recommended for new workspaces diff --git a/src/authorization/Cargo.toml b/src/authorization/Cargo.toml new file mode 100644 index 0000000..b1598cc --- /dev/null +++ b/src/authorization/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "authorization" +version = "0.1.0" +edition = "2024" + +[dependencies] +rhai = { version = "=1.21.0", features = ["std", "sync", "decimal", "internals"] } +heromodels = { path = "../../../db/heromodels" } +heromodels_core = { path = "../../../db/heromodels_core" } +serde = { version = "1.0", features = ["derive"] } diff --git a/src/authorization/src/lib.rs b/src/authorization/src/lib.rs new file mode 100644 index 0000000..ad79f19 --- /dev/null +++ b/src/authorization/src/lib.rs @@ -0,0 +1,197 @@ +//! # Rhai Authorization Crate +//! This crate provides authorization mechanisms for Rhai functions, particularly those interacting with a database. +//! It includes helper functions for authorization checks and macros to simplify the registration +//! of authorized Rhai functions. +//! ## Features: +//! - `is_super_admin`: Checks if a caller (identified by a public key) is a super admin. +//! - `can_access_resource`: Checks if a caller has specific access rights to a resource, using a database connection. +//! - `get_caller_public_key`: Helper to extract `CALLER_PUBLIC_KEY` from the Rhai `NativeCallContext`. +//! - `id_from_i64_to_u32`: Helper to convert `i64` Rhai IDs to `u32` Rust IDs. +//! - `register_authorized_get_by_id_fn!`: Macro to register a Rhai function that retrieves a single item by ID, with authorization checks. +//! - `register_authorized_list_fn!`: Macro to register a Rhai function that lists multiple items, filtering them based on authorization. +//! ## Usage: +//! 1. Use the macros to register your Rhai functions, providing a database connection (`Arc`) and necessary type/name information. +//! 2. The macros internally use `can_access_resource` for authorization checks. +//! 3. Ensure `CALLER_PUBLIC_KEY` is set in the Rhai engine's scope before calling authorized functions. + +use heromodels::models::access::access::can_access_resource; +use heromodels_core::Model; +use rhai::{EvalAltResult, NativeCallContext, Position}; +use std::convert::TryFrom; + +/// Extracts the `CALLER_PUBLIC_KEY` string constant from the Rhai `NativeCallContext`. +/// This key is used to identify the caller for authorization checks. +/// It first checks the current `Scope` and then falls back to the global constants cache. +/// +/// # Arguments +/// * `context`: The Rhai `NativeCallContext` of the currently executing function. +/// + +/// Converts an `i64` (common Rhai integer type) to a `u32` (common Rust ID type). +/// +/// # Arguments +/// * `id_i64`: The `i64` value to convert. +/// +/// # Errors +/// Returns `Err(EvalAltResult::ErrorMismatchDataType)` if the `i64` value cannot be represented as a `u32`. +pub fn id_from_i64_to_u32(id_i64: i64) -> Result> { + u32::try_from(id_i64).map_err(|_| + Box::new(EvalAltResult::ErrorMismatchDataType( + "u32".to_string(), + format!("i64 value ({}) that cannot be represented as u32", id_i64), + Position::NONE, + )) + ) +} + + + + + +/// Extracts the `CALLER_PUBLIC_KEY` string constant from the Rhai `NativeCallContext`'s tag. +/// This key is used to identify the caller for authorization checks. + + + +/// Macro to register a Rhai function that retrieves a single resource by its ID, with authorization. +/// +/// The macro handles: +/// - Argument parsing (ID). +/// - Caller identification via `CALLER_PUBLIC_KEY`. +/// - Authorization check using `AccessControlService::can_access_resource`. +/// - Database call to fetch the resource. +/// - Error handling for type mismatches, authorization failures, DB errors, and not found errors. +/// +/// # Arguments +/// * `module`: Mutable reference to the Rhai `Module`. +/// * `db_clone`: Cloned `Arc` for database access. +/// * `acs_clone`: Cloned `Arc`. +/// * `rhai_fn_name`: String literal for the Rhai function name (e.g., "get_collection"). +/// * `resource_type_str`: String literal for the resource type (e.g., "Collection"), used in authorization checks and error messages. +/// * `db_method_name`: Identifier for the database method to call (e.g., `get_by_id`). +/// * `id_arg_type`: Rust type of the ID argument in Rhai (e.g., `i64`). +/// * `id_rhai_type_name`: String literal for the Rhai type name of the ID (e.g., "i64"), for error messages. +/// * `id_conversion_fn`: Path to a function converting `id_arg_type` to `actual_id_type` (e.g., `id_from_i64_to_u32`). +/// * `actual_id_type`: Rust type of the ID used in the database (e.g., `u32`). +/// * `rhai_return_rust_type`: Rust type of the resource returned by the DB and Rhai function (e.g., `RhaiCollection`). +#[macro_export] +macro_rules! register_authorized_get_by_id_fn { + ( + module: $module:expr, + db_clone: $db_clone:expr, // Cloned Arc for database access + rhai_fn_name: $rhai_fn_name:expr, // String literal for the Rhai function name (e.g., "get_collection") + resource_type_str: $resource_type_str:expr, // String literal for the resource type (e.g., "Collection") + rhai_return_rust_type: $rhai_return_rust_type:ty // Rust type of the resource returned (e.g., `RhaiCollection`) + ) => { + let db_instance_auth = $db_clone.clone(); + let db_instance_fetch = $db_clone.clone(); + + FuncRegistration::new($rhai_fn_name).set_into_module( + $module, + move |context: rhai::NativeCallContext, id_val: i64| -> Result, Box> { + let actual_id: u32 = $crate::id_from_i64_to_u32(id_val)?; + + // Inlined logic to get caller public key + let tag_map = context + .tag() + .and_then(|tag| tag.read_lock::()) + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?; + + let pk_dynamic = tag_map.get("CALLER_PUBLIC_KEY") + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?; + + let caller_pk_str = pk_dynamic.clone().into_string()?; + + // Use the standalone can_access_resource function from heromodels + let has_access = heromodels::models::access::access::can_access_resource( + db_instance_auth.clone(), + &caller_pk_str, + actual_id, + $resource_type_str, + ); + + if !has_access { + return Ok(None); + } + + let result = db_instance_fetch.get_by_id(actual_id).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Database error fetching {}: {:?}", $resource_type_str, e).into(), + context.position(), + )) + })?; + Ok(result) + }, + ); + }; +} + +/// Macro to register a Rhai function that lists all resources of a certain type, with authorization. +/// +/// The macro handles: +/// - Caller identification via `CALLER_PUBLIC_KEY`. +/// - Fetching all items of a specific type from the database. +/// - Filtering the items based on the standalone `can_access_resource` function for each item. +/// - Wrapping the authorized items in a specified collection type (e.g., `RhaiCollectionArray`). +/// - Error handling for DB errors during fetch or authorization checks. +/// +/// # Arguments +/// * `module`: Mutable reference to the Rhai `Module`. +/// * `db_clone`: Cloned `Arc` for database access. +/// * `rhai_fn_name`: String literal for the Rhai function name (e.g., "list_collections"). +/// * `resource_type_str`: String literal for the resource type (e.g., "Collection"), used in authorization checks. +/// * `rhai_return_rust_type`: Rust type of the resource item (e.g., `RhaiCollection`). +/// * `item_id_accessor`: Identifier for the method on `rhai_return_rust_type` that returns its ID (e.g., `id`). +/// * `rhai_return_wrapper_type`: Rust type that wraps a `Vec` of `rhai_return_rust_type` for Rhai (e.g., `RhaiCollectionArray`). +#[macro_export] +macro_rules! register_authorized_list_fn { + ( + module: $module:expr, + db_clone: $db_instance:expr, + rhai_fn_name: $rhai_fn_name:expr, + resource_type_str: $resource_type_str:expr, + rhai_return_rust_type: $rhai_return_rust_type:ty, + item_id_accessor: $item_id_accessor:ident, + rhai_return_wrapper_type: $rhai_return_wrapper_type:ty + ) => { + let db_instance_auth_outer = $db_instance.clone(); + let db_instance_fetch = $db_instance.clone(); + + FuncRegistration::new($rhai_fn_name).set_into_module( + $module, + move |context: rhai::NativeCallContext| -> Result<$rhai_return_wrapper_type, Box> { + // Inlined logic to get caller public key + let tag_map = context + .tag() + .and_then(|tag| tag.read_lock::()) + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?; + + let pk_dynamic = tag_map.get("CALLER_PUBLIC_KEY") + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?; + + let caller_pk_str = pk_dynamic.clone().into_string()?; + + let all_items: Vec<$rhai_return_rust_type> = db_instance_fetch + .collection::<$rhai_return_rust_type>() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("{:?}", e).into(), Position::NONE)))? + .get_all() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("{:?}", e).into(), Position::NONE)))?; + + let authorized_items: Vec<$rhai_return_rust_type> = all_items + .into_iter() + .filter(|item| { + let resource_id = item.$item_id_accessor(); + heromodels::models::access::access::can_access_resource( + db_instance_auth_outer.clone(), + &caller_pk_str, + resource_id, + $resource_type_str, + ) + }) + .collect(); + + Ok(authorized_items.into()) + }, + ); + }; +} diff --git a/src/dsl/Cargo.toml b/src/dsl/Cargo.toml new file mode 100644 index 0000000..c2905a0 --- /dev/null +++ b/src/dsl/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rhailib_dsl" +version = "0.1.0" +edition = "2021" +description = "Central Rhai engine for heromodels" + +[dependencies] +rhai = { version = "=1.21.0", features = ["std", "sync", "decimal", "internals"] } +heromodels = { path = "../../../db/heromodels", features = ["rhai"] } +heromodels_core = { path = "../../../db/heromodels_core" } +chrono = "0.4" +heromodels-derive = { path = "../../../db/heromodels-derive" } +authorization = { path = "../authorization"} +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[dev-dependencies] +tempfile = "3" diff --git a/src/dsl/examples/access/access.rhai b/src/dsl/examples/access/access.rhai new file mode 100644 index 0000000..f34050e --- /dev/null +++ b/src/dsl/examples/access/access.rhai @@ -0,0 +1,16 @@ +// heromodels/examples/access/access.rhai + +print("--- Testing Access Rhai Module ---"); + +// --- Image --- +print("\n1. Creating and saving an access..."); +let access = new_access() + .object_id(1) + .circle_pk("circle_pk") + .group_id(1) + .contact_id(1) + .expires_at(10); + +save_access(access); + +print("Access saved with ID: " + access.id); \ No newline at end of file diff --git a/src/dsl/examples/access/main.rs b/src/dsl/examples/access/main.rs new file mode 100644 index 0000000..dcbe978 --- /dev/null +++ b/src/dsl/examples/access/main.rs @@ -0,0 +1,39 @@ +use heromodels::db::hero::OurDB; +use rhailib_dsl::access::register_access_rhai_module; +use rhai::Engine; +use std::sync::Arc; +use std::{fs, path::Path}; + +fn main() -> Result<(), Box> { + // Initialize Rhai engine + let mut engine = Engine::new(); + + // Initialize database with OurDB + let db_path = "temp_access_db"; + // Clean up previous database file if it exists + if Path::new(db_path).exists() { + fs::remove_dir_all(db_path)?; + } + let db = Arc::new(OurDB::new(db_path, true).expect("Failed to create database")); + + // Register the library module with Rhai + register_access_rhai_module(&mut engine, db.clone()); + + // Load and evaluate the Rhai script + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let script_path = Path::new(manifest_dir).join("examples").join("access").join("access.rhai"); + println!("Script path: {}", script_path.display()); + let script = fs::read_to_string(&script_path)?; + + println!("--- Running Access Rhai Script ---"); + match engine.eval::<()>(&script) { + Ok(_) => println!("\n--- Script executed successfully! ---"), + Err(e) => eprintln!("\n--- Script execution failed: {} ---", e), + } + + // Clean up the database file + fs::remove_dir_all(db_path)?; + println!("--- Cleaned up temporary database. ---"); + + Ok(()) +} diff --git a/src/dsl/examples/library.rs b/src/dsl/examples/library.rs new file mode 100644 index 0000000..7192558 --- /dev/null +++ b/src/dsl/examples/library.rs @@ -0,0 +1,166 @@ +use rhai::{Engine, Module, Position, Scope, Dynamic}; +use std::sync::Arc; +use tempfile::tempdir; + +// Import DB traits with an alias for the Collection trait to avoid naming conflicts. +// Import DB traits from heromodels::db as suggested by compiler errors. +use heromodels::db::{Db, Collection as DbCollection}; +use heromodels::{ + db::hero::OurDB, + models::library::collection::Collection, // Actual data model for single items + models::library::rhai::RhaiCollectionArray, // Wrapper for arrays of collections + models::access::access::Access, +}; +// Import macros and the functions they depend on, which must be in scope during invocation. +use rhailib_dsl::{register_authorized_get_by_id_fn, register_authorized_list_fn}; + +use rhai::{FuncRegistration, EvalAltResult}; // For macro expansion + +// Rewritten to match the new `Access` model structure. +fn grant_access(db: &Arc, user_pk: &str, resource_type: &str, resource_id: u32) { + let access_record = Access::new() + .circle_pk(user_pk.to_string()) + .object_type(resource_type.to_string()) + .object_id(resource_id) + .contact_id(0) + .group_id(0); + + db.set(&access_record).expect("Failed to set access record"); +} + +// No changes needed here, but it relies on the new imports to compile. +fn register_example_module(engine: &mut Engine, db: Arc) { + let mut module = Module::new(); + + register_authorized_get_by_id_fn!( + module: &mut module, + db_clone: db.clone(), + rhai_fn_name: "get_collection", + resource_type_str: "Collection", + rhai_return_rust_type: heromodels::models::library::collection::Collection // Use Collection struct + ); + + register_authorized_list_fn!( + module: &mut module, + db_clone: db.clone(), + rhai_fn_name: "list_all_collections", + resource_type_str: "Collection", + rhai_return_rust_type: heromodels::models::library::collection::Collection, // Use Collection struct + item_id_accessor: id, // Assumes Collection has an id() method that returns u32 + rhai_return_wrapper_type: heromodels::models::library::rhai::RhaiCollectionArray // Wrapper type for Rhai + ); + + engine.register_global_module(module.into()); +} + +fn main() -> Result<(), Box> { + let mut engine = Engine::new(); + + let temp_dir = tempdir().unwrap(); + let db = Arc::new(OurDB::new(temp_dir.path(), false).expect("Failed to create DB")); + + register_example_module(&mut engine, db.clone()); + + println!("--- Registered Functions ---"); + // The closure now returns Option by cloning the metadata. + // FuncMetadata is Clone and 'static, satisfying collect_fn_metadata's requirements. + for metadata_clone in engine.collect_fn_metadata(None, |info: rhai::FuncInfo<'_>| Some(info.metadata.clone()), true) { + if metadata_clone.name == "get_collection" { + println!("Found get_collection function, args: {:?}", metadata_clone.param_types); + } + } + println!("--------------------------"); + + + // Populate DB using the new `create_collection` helper. + // Ownership is no longer on the collection itself, so we don't need owner_pk here. + let coll = Collection::new() + .title("My new collection") + .description("This is a new collection"); + + db.set(&coll).expect("Failed to set collection"); + + let coll1 = Collection::new() + .title("Alice's Private Collection") + .description("This is Alice's private collection"); + let coll2 = Collection::new() + .title("Bob's Shared Collection") + .description("This is Bob's shared collection"); + let coll3 = Collection::new() + .title("General Collection") + .description("This is a general collection"); + + db.set(&coll1).expect("Failed to set collection"); + db.set(&coll2).expect("Failed to set collection"); + db.set(&coll3).expect("Failed to set collection"); + + // Grant access based on the new model. + grant_access(&db, "alice_pk", "Collection", coll1.id()); + grant_access(&db, "bob_pk", "Collection", coll2.id()); + grant_access(&db, "alice_pk", "Collection", coll2.id()); // Alice can also see Bob's collection. + grant_access(&db, "general_user_pk", "Collection", coll3.id()); + + println!("--- Rhai Authorization Example ---"); + + let mut scope = Scope::new(); + + // Scenario 1: Alice accesses her own collection (Success) + let mut db_config = rhai::Map::new(); + db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into()); + engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions + + // Create a Dynamic value holding your DB path or a config object + let mut db_config = rhai::Map::new(); + db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into()); + db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + + println!("Alice accessing her collection 1: Success, title"); // Access field directly + let result = engine.eval::>("get_collection(1)")?; + let result_clone = result.clone().expect("REASON"); + println!("Alice accessing her collection 1: Success, title = {}", result_clone.title); // Access field directly + assert_eq!(result_clone.id(), 1); + + // Scenario 2: Bob tries to access Alice's collection (Failure) + let mut db_config = rhai::Map::new(); + db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into()); + db_config.insert("CALLER_PUBLIC_KEY".into(), "bob_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + let result = engine.eval_with_scope::(&mut scope, "get_collection(1)"); + println!("Bob accessing Alice's collection 1: {:?}", result); + let result_clone = result.expect("REASON"); + println!("Bob accessing Alice's collection 1: {:?}", result_clone); + // assert!(result_clone.is_none()); + + // Scenario 3: Alice accesses Bob's collection (Success) + let mut db_config = rhai::Map::new(); + db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into()); + db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + let result = engine.eval_with_scope::(&mut scope, "get_collection(2)")?; + println!("Alice accessing Bob's collection 2: Success, title = {}", result.title); // Access field directly + assert_eq!(result.id(), 2); + + // Scenario 4: General user lists collections (Sees 1) + let mut db_config = rhai::Map::new(); + db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into()); + db_config.insert("CALLER_PUBLIC_KEY".into(), "general_user_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + let result = engine.eval_with_scope::(&mut scope, "list_all_collections()").unwrap(); + println!("General user listing collections: Found {}", result.0.len()); + assert_eq!(result.0.len(), 1); + assert_eq!(result.0[0].id(), 3); + + // Scenario 5: Alice lists collections (Sees 2) + let mut db_config = rhai::Map::new(); + db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into()); + db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + let collections = engine.eval_with_scope::(&mut scope, "list_all_collections()").unwrap(); + println!("Alice listing collections: Found {}", collections.0.len()); + assert_eq!(collections.0.len(), 2); + let ids: Vec = collections.0.iter().map(|c| c.id()).collect(); + assert!(ids.contains(&1) && ids.contains(&2)); + + Ok(()) +} diff --git a/src/dsl/expanded_library.rs b/src/dsl/expanded_library.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/dsl/src/access.rs b/src/dsl/src/access.rs new file mode 100644 index 0000000..c58f2bc --- /dev/null +++ b/src/dsl/src/access.rs @@ -0,0 +1,263 @@ +use heromodels::db::Db; +use rhai::plugin::*; +use rhai::{Array, Dynamic, Engine, EvalAltResult, INT, Module, Position}; +use std::mem; +use std::sync::Arc; + +use heromodels::models::access::Access; +type RhaiAccess = Access; +use heromodels::db::Collection; +use heromodels::db::hero::OurDB; + +// Helper to convert i64 from Rhai to u32 for IDs +fn id_from_i64_to_u32(id_i64: i64) -> Result> { + u32::try_from(id_i64).map_err(|_| { + Box::new(EvalAltResult::ErrorArithmetic( + format!("Failed to convert ID '{}' to u32", id_i64).into(), + Position::NONE, + )) + }) +} + +#[export_module] +mod rhai_access_module { + // --- Access Functions --- + #[rhai_fn(name = "new_access", return_raw)] + pub fn new_access() -> Result> { + let access = Access::new(); + Ok(access) + } + + /// Sets the access name + #[rhai_fn(name = "object_id", return_raw, global, pure)] + pub fn set_object_id( + access: &mut RhaiAccess, + object_id: i64, + ) -> Result> { + let id = id_from_i64_to_u32(object_id)?; + let owned_access = std::mem::take(access); + *access = owned_access.object_id(id); + Ok(access.clone()) + } + + /// Sets the access name + #[rhai_fn(name = "circle_pk", return_raw, global, pure)] + pub fn set_circle_pk( + access: &mut RhaiAccess, + circle_pk: String, + ) -> Result> { + let owned_access = std::mem::take(access); + *access = owned_access.circle_pk(circle_pk); + Ok(access.clone()) + } + + /// Sets the access name + #[rhai_fn(name = "group_id", return_raw, global, pure)] + pub fn set_group_id( + access: &mut RhaiAccess, + group_id: i64, + ) -> Result> { + let id = id_from_i64_to_u32(group_id)?; + let owned_access = std::mem::take(access); + *access = owned_access.group_id(id); + Ok(access.clone()) + } + + #[rhai_fn(name = "contact_id", return_raw, global, pure)] + pub fn set_contact_id( + access: &mut RhaiAccess, + contact_id: i64, + ) -> Result> { + let id = id_from_i64_to_u32(contact_id)?; + let owned_access = std::mem::take(access); + *access = owned_access.contact_id(id); + Ok(access.clone()) + } + + #[rhai_fn(name = "expires_at", return_raw, global, pure)] + pub fn set_expires_at( + access: &mut RhaiAccess, + expires_at: i64, + ) -> Result> { + let expires_at = expires_at as u64; + let owned_access = std::mem::take(access); + *access = owned_access.expires_at(Some(expires_at)); + Ok(access.clone()) + } + + // Access Getters + #[rhai_fn(get = "id", pure)] + pub fn get_access_id(access: &mut RhaiAccess) -> i64 { + access.base_data.id as i64 + } + + #[rhai_fn(get = "object_id", pure)] + pub fn get_access_object_id(access: &mut RhaiAccess) -> i64 { + access.object_id as i64 + } + + #[rhai_fn(get = "circle_pk", pure)] + pub fn get_access_circle_pk(access: &mut RhaiAccess) -> String { + access.circle_pk.clone() + } + + #[rhai_fn(get = "group_id", pure)] + pub fn get_access_group_id(access: &mut RhaiAccess) -> i64 { + access.group_id as i64 + } + + #[rhai_fn(get = "contact_id", pure)] + pub fn get_access_contact_id(access: &mut RhaiAccess) -> i64 { + access.contact_id as i64 + } + + #[rhai_fn(get = "expires_at", pure)] + pub fn get_access_expires_at(access: &mut RhaiAccess) -> i64 { + access.expires_at.unwrap_or(0) as i64 + } + + #[rhai_fn(get = "created_at", pure)] + pub fn get_access_created_at(access: &mut RhaiAccess) -> i64 { + access.base_data.created_at + } + + #[rhai_fn(get = "modified_at", pure)] + pub fn get_access_modified_at(access: &mut RhaiAccess) -> i64 { + access.base_data.modified_at + } +} + +// // A function that takes the call context and an integer argument. +// fn save_access(context: NativeCallContext, access: RhaiAccess) -> Result<(), Box> { +// let optional_tag_ref: Option<&Dynamic> = context.tag(); + +// // Ensure the tag exists +// let tag_ref: &Dynamic = optional_tag_ref.ok_or_else(|| { +// Box::new(EvalAltResult::ErrorRuntime( +// "Custom tag not set for this evaluation run.".into(), +// context.position(), // Use context.position() if available and relevant +// )) +// })?; + +// // Initialize database with OurDB for the Rhai engine +// // Using a temporary/in-memory like database for the worker +// let tag_map = tag_ref.read_lock::().ok_or_else(|| { +// Box::new(EvalAltResult::ErrorRuntime( +// "Tag is not a Map or is locked".into(), +// Position::NONE, +// )) +// })?; + +// let db_path = tag_map.get("CIRCLE_DB_PATH").expect("CIRCLE_DB_PATH not found").as_str().to_string(); +// let db = Arc::new( +// OurDB::new(db_path, false) +// .expect("Failed to create temporary DB for Rhai engine"), +// ); + +// let result = db.set(&access).map_err(|e| { +// Box::new(EvalAltResult::ErrorRuntime( +// format!("DB Error set_access: {}", e).into(), +// Position::NONE, +// )) +// })?; + +// // Return the updated access with the correct ID +// Ok(result) +// } + + +pub fn register_access_rhai_module(engine: &mut Engine, db: Arc) { + // Register the exported module globally + let module = exported_module!(rhai_access_module); + engine.register_global_module(module.into()); + + // Create a module for database functions + let mut db_module = Module::new(); + + // let db_clone_set_access = db.clone(); + // db_module.set_native_fn( + // "save_access", + // move |access: Access| -> Result> { + // // Use the Collection trait method directly + // let result = db_clone_set_access.set(&access).map_err(|e| { + // Box::new(EvalAltResult::ErrorRuntime( + // format!("DB Error set_access: {}", e).into(), + // Position::NONE, + // )) + // })?; + + // // Return the updated access with the correct ID + // Ok(result.1) + // }, + // ); + + // Manually register database functions as they need to capture 'db' + let db_clone_delete_access = db.clone(); + db_module.set_native_fn( + "delete_access", + move |access: Access| -> Result<(), Box> { + // Use the Collection trait method directly + let result = db_clone_delete_access + .collection::() + .expect("can open access collection") + .delete_by_id(access.base_data.id) + .expect("can delete event"); + + // Return the updated event with the correct ID + Ok(result) + }, + ); + + let db_clone_get_access = db.clone(); + db_module.set_native_fn( + "get_access_by_id", + move |id_i64: INT| -> Result> { + let id_u32 = id_from_i64_to_u32(id_i64)?; + // Use the Collection trait method directly + db_clone_get_access + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error get_access_by_id: {}", e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Access with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); + + // Add list_accesss function to get all accesss + let db_clone_list_accesss = db.clone(); + db_module.set_native_fn( + "list_accesss", + move || -> Result> { + let collection = db_clone_list_accesss.collection::().map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get access collection: {:?}", e).into(), + Position::NONE, + )) + })?; + let accesss = collection.get_all().map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get all accesss: {:?}", e).into(), + Position::NONE, + )) + })?; + let mut array = Array::new(); + for access in accesss { + array.push(Dynamic::from(access)); + } + Ok(Dynamic::from(array)) + }, + ); + + // Register the database module globally + engine.register_global_module(db_module.into()); + + println!("Successfully registered access Rhai module using export_module approach."); +} diff --git a/src/dsl/src/lib.rs b/src/dsl/src/lib.rs new file mode 100644 index 0000000..4161203 --- /dev/null +++ b/src/dsl/src/lib.rs @@ -0,0 +1,6 @@ +pub mod library; +pub mod access; + +pub use authorization::register_authorized_get_by_id_fn; +pub use authorization::register_authorized_list_fn; +pub use authorization::id_from_i64_to_u32; \ No newline at end of file diff --git a/src/dsl/src/library.rs b/src/dsl/src/library.rs new file mode 100644 index 0000000..57c8dc9 --- /dev/null +++ b/src/dsl/src/library.rs @@ -0,0 +1,1031 @@ +use rhai::plugin::*; +use rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, Position, TypeBuilder}; +use serde::Serialize; +use heromodels::db::Db; +use serde_json; +use std::mem; +use std::sync::Arc; + + +use heromodels::models::library::collection::Collection as RhaiCollection; +use heromodels::models::library::items::{ + Book as RhaiBook, Image as RhaiImage, Markdown as RhaiMarkdown, Pdf as RhaiPdf, + Slides as RhaiSlides, TocEntry as RhaiTocEntry, +}; +use heromodels::db::Collection as DbCollectionTrait; +use heromodels::db::hero::OurDB; + + +// Helper to convert i64 from Rhai to u32 for IDs +fn id_from_i64_to_u32(id_i64: i64) -> Result> { + u32::try_from(id_i64).map_err(|_| { + Box::new(EvalAltResult::ErrorMismatchDataType( + "u32".to_string(), // Expected type + format!("i64 value ({}) that cannot be represented as u32", id_i64), // Actual type/value description + Position::NONE, + )) + }) +} + +/// Registers a `.json()` method for any type `T` that implements the required traits. +fn register_json_method(engine: &mut Engine) +where + // The type must be: + T: CustomType + Clone + Serialize, // A clonable, serializable, custom type for Rhai +{ + // This is the function that will be called when a script runs '.json()' + let to_json_fn = |obj: &mut T| -> Result> { + // Use serde_json to serialize the object to a pretty-formatted string. + // The '?' will automatically convert any serialization error into a Rhai error. + serde_json::to_string_pretty(obj).map_err(|e| e.to_string().into()) + }; + + // Register the function as a method named "json" for the type 'T'. + engine.build_type::().register_fn("json", to_json_fn); +} + +// Wrapper type for a list of collections to enable .json() method via register_json_method +#[derive(Debug, Clone, Serialize, CustomType)] +#[rhai_type(name = "CollectionArray")] +pub struct RhaiCollectionArray(pub Vec); + +#[export_module] +mod rhai_library_module { + // --- Collection Functions --- + #[rhai_fn(name = "new_collection")] + pub fn new_collection() -> RhaiCollection { + RhaiCollection::new() + } + + #[rhai_fn(name = "title", return_raw, global, pure)] + pub fn collection_title( + collection: &mut RhaiCollection, + title: String, + ) -> Result> { + let owned = mem::take(collection); + *collection = owned.title(title); + Ok(collection.clone()) + } + + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn collection_description( + collection: &mut RhaiCollection, + description: String, + ) -> Result> { + let owned = mem::take(collection); + *collection = owned.description(description); + Ok(collection.clone()) + } + + #[rhai_fn(name = "add_image", return_raw, global, pure)] + pub fn collection_add_image( + collection: &mut RhaiCollection, + image_id: i64, + ) -> Result> { + let id = id_from_i64_to_u32(image_id)?; + let owned = mem::take(collection); + *collection = owned.add_image(id); + Ok(collection.clone()) + } + + #[rhai_fn(name = "add_pdf", return_raw, global, pure)] + pub fn collection_add_pdf( + collection: &mut RhaiCollection, + pdf_id: i64, + ) -> Result> { + let id = id_from_i64_to_u32(pdf_id)?; + let owned = mem::take(collection); + *collection = owned.add_pdf(id); + Ok(collection.clone()) + } + + #[rhai_fn(name = "add_markdown", return_raw, global, pure)] + pub fn collection_add_markdown( + collection: &mut RhaiCollection, + markdown_id: i64, + ) -> Result> { + let id = id_from_i64_to_u32(markdown_id)?; + let owned = mem::take(collection); + *collection = owned.add_markdown(id); + Ok(collection.clone()) + } + + #[rhai_fn(name = "add_book", return_raw, global, pure)] + pub fn collection_add_book( + collection: &mut RhaiCollection, + book_id: i64, + ) -> Result> { + let id = id_from_i64_to_u32(book_id)?; + let owned = mem::take(collection); + *collection = owned.add_book(id); + Ok(collection.clone()) + } + + #[rhai_fn(name = "add_slides", return_raw, global, pure)] + pub fn collection_add_slides( + collection: &mut RhaiCollection, + slides_id: i64, + ) -> Result> { + let id = id_from_i64_to_u32(slides_id)?; + let owned = mem::take(collection); + *collection = owned.add_slides(id); + Ok(collection.clone()) + } + + #[rhai_fn(get = "id", pure)] + pub fn get_collection_id(collection: &mut RhaiCollection) -> i64 { + collection.base_data.id as i64 + } + + #[rhai_fn(get = "created_at", pure)] + pub fn get_collection_created_at(collection: &mut RhaiCollection) -> i64 { + collection.base_data.created_at + } + + #[rhai_fn(get = "modified_at", pure)] + pub fn get_collection_modified_at(collection: &mut RhaiCollection) -> i64 { + collection.base_data.modified_at + } + + #[rhai_fn(get = "title", pure)] + pub fn get_collection_title(collection: &mut RhaiCollection) -> String { + collection.title.clone() + } + + #[rhai_fn(get = "description", pure)] + pub fn get_collection_description(collection: &mut RhaiCollection) -> Option { + collection.description.clone() + } + + #[rhai_fn(get = "images", pure)] + pub fn get_collection_images(collection: &mut RhaiCollection) -> Vec { + collection + .images + .clone() + .into_iter() + .map(|id| id as i64) + .collect() + } + + #[rhai_fn(get = "pdfs", pure)] + pub fn get_collection_pdfs(collection: &mut RhaiCollection) -> Vec { + collection + .pdfs + .clone() + .into_iter() + .map(|id| id as i64) + .collect() + } + + #[rhai_fn(get = "markdowns", pure)] + pub fn get_collection_markdowns(collection: &mut RhaiCollection) -> Vec { + collection + .markdowns + .clone() + .into_iter() + .map(|id| id as i64) + .collect() + } + + #[rhai_fn(get = "books", pure)] + pub fn get_collection_books(collection: &mut RhaiCollection) -> Vec { + collection + .books + .clone() + .into_iter() + .map(|id| id as i64) + .collect() + } + + #[rhai_fn(get = "slides", pure)] + pub fn get_collection_slides(collection: &mut RhaiCollection) -> Vec { + collection + .slides + .clone() + .into_iter() + .map(|id| id as i64) + .collect() + } + + // --- Image Functions --- + #[rhai_fn(name = "new_image")] + pub fn new_image() -> RhaiImage { + RhaiImage::new() + } + + #[rhai_fn(name = "title", return_raw, global, pure)] + pub fn image_title( + image: &mut RhaiImage, + title: String, + ) -> Result> { + let owned = mem::take(image); + *image = owned.title(title); + Ok(image.clone()) + } + + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn image_description( + image: &mut RhaiImage, + description: String, + ) -> Result> { + let owned = mem::take(image); + *image = owned.description(description); + Ok(image.clone()) + } + + #[rhai_fn(name = "url", return_raw, global, pure)] + pub fn image_url(image: &mut RhaiImage, url: String) -> Result> { + let owned = mem::take(image); + *image = owned.url(url); + Ok(image.clone()) + } + + #[rhai_fn(name = "width", return_raw, global, pure)] + pub fn image_width(image: &mut RhaiImage, width: i64) -> Result> { + let owned = mem::take(image); + *image = owned.width(width as u32); + Ok(image.clone()) + } + + #[rhai_fn(name = "height", return_raw, global, pure)] + pub fn image_height( + image: &mut RhaiImage, + height: i64, + ) -> Result> { + let owned = mem::take(image); + *image = owned.height(height as u32); + Ok(image.clone()) + } + + #[rhai_fn(get = "id", pure)] + pub fn get_image_id(image: &mut RhaiImage) -> i64 { + image.base_data.id as i64 + } + + #[rhai_fn(get = "created_at", pure)] + pub fn get_image_created_at(image: &mut RhaiImage) -> i64 { + image.base_data.created_at + } + + #[rhai_fn(get = "modified_at", pure)] + pub fn get_image_modified_at(image: &mut RhaiImage) -> i64 { + image.base_data.modified_at + } + + #[rhai_fn(get = "title", pure)] + pub fn get_image_title(image: &mut RhaiImage) -> String { + image.title.clone() + } + + #[rhai_fn(get = "description", pure)] + pub fn get_image_description(image: &mut RhaiImage) -> Option { + image.description.clone() + } + + #[rhai_fn(get = "url", pure)] + pub fn get_image_url(image: &mut RhaiImage) -> String { + image.url.clone() + } + + #[rhai_fn(get = "width", pure)] + pub fn get_image_width(image: &mut RhaiImage) -> u32 { + image.width + } + + #[rhai_fn(get = "height", pure)] + pub fn get_image_height(image: &mut RhaiImage) -> u32 { + image.height + } + + // --- Pdf Functions --- + #[rhai_fn(name = "new_pdf")] + pub fn new_pdf() -> RhaiPdf { + RhaiPdf::new() + } + + #[rhai_fn(name = "title", return_raw, global, pure)] + pub fn pdf_title(pdf: &mut RhaiPdf, title: String) -> Result> { + let owned = mem::take(pdf); + *pdf = owned.title(title); + Ok(pdf.clone()) + } + + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn pdf_description( + pdf: &mut RhaiPdf, + description: String, + ) -> Result> { + let owned = mem::take(pdf); + *pdf = owned.description(description); + Ok(pdf.clone()) + } + + #[rhai_fn(name = "url", return_raw, global, pure)] + pub fn pdf_url(pdf: &mut RhaiPdf, url: String) -> Result> { + let owned = mem::take(pdf); + *pdf = owned.url(url); + Ok(pdf.clone()) + } + + #[rhai_fn(name = "page_count", return_raw, global, pure)] + pub fn pdf_page_count( + pdf: &mut RhaiPdf, + page_count: i64, + ) -> Result> { + let owned = mem::take(pdf); + *pdf = owned.page_count(page_count as u32); + Ok(pdf.clone()) + } + + #[rhai_fn(get = "id", pure)] + pub fn get_pdf_id(pdf: &mut RhaiPdf) -> i64 { + pdf.base_data.id as i64 + } + + #[rhai_fn(get = "created_at", pure)] + pub fn get_pdf_created_at(pdf: &mut RhaiPdf) -> i64 { + pdf.base_data.created_at + } + + #[rhai_fn(get = "modified_at", pure)] + pub fn get_pdf_modified_at(pdf: &mut RhaiPdf) -> i64 { + pdf.base_data.modified_at + } + + #[rhai_fn(get = "title", pure)] + pub fn get_pdf_title(pdf: &mut RhaiPdf) -> String { + pdf.title.clone() + } + + #[rhai_fn(get = "description", pure)] + pub fn get_pdf_description(pdf: &mut RhaiPdf) -> Option { + pdf.description.clone() + } + + #[rhai_fn(get = "url", pure)] + pub fn get_pdf_url(pdf: &mut RhaiPdf) -> String { + pdf.url.clone() + } + + #[rhai_fn(get = "page_count", pure)] + pub fn get_pdf_page_count(pdf: &mut RhaiPdf) -> u32 { + pdf.page_count + } + + // --- Markdown Functions --- + #[rhai_fn(name = "new_markdown")] + pub fn new_markdown() -> RhaiMarkdown { + RhaiMarkdown::new() + } + + #[rhai_fn(name = "title", return_raw, global, pure)] + pub fn markdown_title( + markdown: &mut RhaiMarkdown, + title: String, + ) -> Result> { + let owned = mem::take(markdown); + *markdown = owned.title(title); + Ok(markdown.clone()) + } + + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn markdown_description( + markdown: &mut RhaiMarkdown, + description: String, + ) -> Result> { + let owned = mem::take(markdown); + *markdown = owned.description(description); + Ok(markdown.clone()) + } + + #[rhai_fn(name = "content", return_raw, global, pure)] + pub fn markdown_content( + markdown: &mut RhaiMarkdown, + content: String, + ) -> Result> { + let owned = mem::take(markdown); + *markdown = owned.content(content); + Ok(markdown.clone()) + } + + #[rhai_fn(get = "id", pure)] + pub fn get_markdown_id(markdown: &mut RhaiMarkdown) -> i64 { + markdown.base_data.id as i64 + } + + #[rhai_fn(get = "created_at", pure)] + pub fn get_markdown_created_at(markdown: &mut RhaiMarkdown) -> i64 { + markdown.base_data.created_at + } + + #[rhai_fn(get = "modified_at", pure)] + pub fn get_markdown_modified_at(markdown: &mut RhaiMarkdown) -> i64 { + markdown.base_data.modified_at + } + + #[rhai_fn(get = "title", pure)] + pub fn get_markdown_title(markdown: &mut RhaiMarkdown) -> String { + markdown.title.clone() + } + + #[rhai_fn(get = "description", pure)] + pub fn get_markdown_description(markdown: &mut RhaiMarkdown) -> Option { + markdown.description.clone() + } + + #[rhai_fn(get = "content", pure)] + pub fn get_markdown_content(markdown: &mut RhaiMarkdown) -> String { + markdown.content.clone() + } + + // --- TocEntry Functions --- + #[rhai_fn(name = "new_toc_entry")] + pub fn new_toc_entry() -> RhaiTocEntry { + RhaiTocEntry::new() + } + + #[rhai_fn(name = "title", return_raw, global, pure)] + pub fn toc_entry_title( + entry: &mut RhaiTocEntry, + title: String, + ) -> Result> { + let owned = mem::take(entry); + *entry = owned.title(title); + Ok(entry.clone()) + } + + #[rhai_fn(name = "page", return_raw, global, pure)] + pub fn toc_entry_page( + entry: &mut RhaiTocEntry, + page: i64, + ) -> Result> { + let owned = mem::take(entry); + *entry = owned.page(page as u32); + Ok(entry.clone()) + } + + #[rhai_fn(name = "add_subsection", return_raw, global, pure)] + pub fn toc_entry_add_subsection( + entry: &mut RhaiTocEntry, + subsection: RhaiTocEntry, + ) -> Result> { + let owned = mem::take(entry); + *entry = owned.add_subsection(subsection); + Ok(entry.clone()) + } + + #[rhai_fn(get = "title", pure)] + pub fn get_toc_entry_title(entry: &mut RhaiTocEntry) -> String { + entry.title.clone() + } + + #[rhai_fn(get = "page", pure)] + pub fn get_toc_entry_page(entry: &mut RhaiTocEntry) -> u32 { + entry.page + } + + #[rhai_fn(get = "subsections", pure)] + pub fn get_toc_entry_subsections(entry: &mut RhaiTocEntry) -> Vec { + entry.subsections.clone() + } + + // --- Book Functions --- + #[rhai_fn(name = "new_book")] + pub fn new_book() -> RhaiBook { + RhaiBook::new() + } + + #[rhai_fn(name = "title", return_raw, global, pure)] + pub fn book_title(book: &mut RhaiBook, title: String) -> Result> { + let owned = mem::take(book); + *book = owned.title(title); + Ok(book.clone()) + } + + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn book_description( + book: &mut RhaiBook, + description: String, + ) -> Result> { + let owned = mem::take(book); + *book = owned.description(description); + Ok(book.clone()) + } + + #[rhai_fn(name = "add_page", return_raw, global, pure)] + pub fn book_add_page( + book: &mut RhaiBook, + content: String, + ) -> Result> { + let owned = mem::take(book); + *book = owned.add_page(content); + Ok(book.clone()) + } + + #[rhai_fn(name = "add_toc_entry", return_raw, global, pure)] + pub fn book_add_toc_entry( + book: &mut RhaiBook, + entry: RhaiTocEntry, + ) -> Result> { + let owned = mem::take(book); + *book = owned.add_toc_entry(entry); + Ok(book.clone()) + } + + #[rhai_fn(get = "id", pure)] + pub fn get_book_id(book: &mut RhaiBook) -> i64 { + book.base_data.id as i64 + } + + #[rhai_fn(get = "created_at", pure)] + pub fn get_book_created_at(book: &mut RhaiBook) -> i64 { + book.base_data.created_at + } + + #[rhai_fn(get = "modified_at", pure)] + pub fn get_book_modified_at(book: &mut RhaiBook) -> i64 { + book.base_data.modified_at + } + + #[rhai_fn(get = "title", pure)] + pub fn get_book_title(book: &mut RhaiBook) -> String { + book.title.clone() + } + + #[rhai_fn(get = "description", pure)] + pub fn get_book_description(book: &mut RhaiBook) -> Option { + book.description.clone() + } + + #[rhai_fn(get = "table_of_contents", pure)] + pub fn get_book_table_of_contents(book: &mut RhaiBook) -> Vec { + book.table_of_contents.clone() + } + + #[rhai_fn(get = "pages", pure)] + pub fn get_book_pages(book: &mut RhaiBook) -> Vec { + book.pages.clone() + } + + // --- Slides Functions --- + #[rhai_fn(name = "new_slides")] + pub fn new_slides() -> RhaiSlides { + RhaiSlides::new() + } + + #[rhai_fn(name = "title", return_raw, global, pure)] + pub fn slides_title( + slides: &mut RhaiSlides, + title: String, + ) -> Result> { + let owned = mem::take(slides); + *slides = owned.title(title); + Ok(slides.clone()) + } + + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn slides_description( + slides: &mut RhaiSlides, + description: String, + ) -> Result> { + let owned = mem::take(slides); + *slides = owned.description(description); + Ok(slides.clone()) + } + + #[rhai_fn(name = "add_slide", return_raw, global, pure)] + pub fn slides_add_slide( + slides: &mut RhaiSlides, + url: String, + title: String, + ) -> Result> { + let owned = mem::take(slides); + let title_opt = if title.is_empty() { None } else { Some(title) }; + *slides = owned.add_slide(url, title_opt); + Ok(slides.clone()) + } + + #[rhai_fn(name = "add_slide", return_raw, global, pure)] + pub fn slides_add_slide_no_title( + slides: &mut RhaiSlides, + url: String, + ) -> Result> { + let owned = mem::take(slides); + *slides = owned.add_slide(url, None); + Ok(slides.clone()) + } + + #[rhai_fn(get = "id", pure)] + pub fn get_slides_id(slides: &mut RhaiSlides) -> i64 { + slides.base_data.id as i64 + } + + #[rhai_fn(get = "created_at", pure)] + pub fn get_slides_created_at(slides: &mut RhaiSlides) -> i64 { + slides.base_data.created_at + } + + #[rhai_fn(get = "modified_at", pure)] + pub fn get_slides_modified_at(slides: &mut RhaiSlides) -> i64 { + slides.base_data.modified_at + } + + #[rhai_fn(get = "title", pure)] + pub fn get_slides_title(slides: &mut RhaiSlides) -> String { + slides.title.clone() + } + + #[rhai_fn(get = "description", pure)] + pub fn get_slides_description(slides: &mut RhaiSlides) -> Option { + slides.description.clone() + } + + #[rhai_fn(get = "slide_urls", pure)] + pub fn get_slides_slide_urls(slides: &mut RhaiSlides) -> Vec { + slides.slide_urls.clone() + } + + #[rhai_fn(get = "slide_titles", pure)] + pub fn get_slides_slide_titles(slides: &mut RhaiSlides) -> Vec> { + slides.slide_titles.clone() + } +} + +pub fn register_library_rhai_module(engine: &mut Engine, db: Arc) { + let module = exported_module!(rhai_library_module); + engine.register_global_module(module.into()); + + let mut db_module = Module::new(); + + register_json_method::(engine); + register_json_method::(engine); + register_json_method::(engine); + register_json_method::(engine); + register_json_method::(engine); + register_json_method::(engine); + register_json_method::(engine); + + // Register .json() method for our custom CollectionArray type + register_json_method::(engine); + + // --- Collection DB Functions --- + let db_clone = db.clone(); + db_module.set_native_fn( + "save_collection", + move |collection: RhaiCollection| -> Result> { + let result = db_clone.set(&collection).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(result.1) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "get_collection", + move |id: i64| -> Result> { + let collection_id = id_from_i64_to_u32(id)?; + db_clone + .get_by_id(collection_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Collection with ID {} not found", collection_id).into(), + Position::NONE, + )) + }) + }, + ); + + let db_clone_list_collections = db.clone(); + db_module.set_native_fn( + "list_collections", + move || -> Result> { + let collections_vec: Vec = db_clone_list_collections + .collection::() + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error (list_collections - access): {:?}", e).into(), + Position::NONE, + )) + })? + .get_all() + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error (list_collections - get_all): {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(RhaiCollectionArray(collections_vec)) // Wrap in RhaiCollectionArray + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "delete_collection", + move |id: i64| -> Result<(), Box> { + let collection_id = id_from_i64_to_u32(id)?; + db_clone + .collection::() + .unwrap() + .delete_by_id(collection_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(()) + }, + ); + + // --- Image DB Functions --- + let db_clone = db.clone(); + db_module.set_native_fn( + "save_image", + move |image: RhaiImage| -> Result> { + let result = db_clone.set(&image).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(result.1) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "get_image", + move |id: i64| -> Result> { + let image_id = id_from_i64_to_u32(id)?; + db_clone + .get_by_id(image_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Image with ID {} not found", image_id).into(), + Position::NONE, + )) + }) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "delete_image", + move |id: i64| -> Result<(), Box> { + let image_id = id_from_i64_to_u32(id)?; + db_clone + .collection::() + .unwrap() + .delete_by_id(image_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(()) + }, + ); + + // --- Pdf DB Functions --- + let db_clone = db.clone(); + db_module.set_native_fn( + "save_pdf", + move |pdf: RhaiPdf| -> Result> { + let result = db_clone.set(&pdf).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(result.1) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "get_pdf", + move |id: i64| -> Result> { + let pdf_id = id_from_i64_to_u32(id)?; + db_clone + .get_by_id(pdf_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Pdf with ID {} not found", pdf_id).into(), + Position::NONE, + )) + }) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "delete_pdf", + move |id: i64| -> Result<(), Box> { + let pdf_id = id_from_i64_to_u32(id)?; + db_clone + .collection::() + .unwrap() + .delete_by_id(pdf_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(()) + }, + ); + + // --- Markdown DB Functions --- + let db_clone = db.clone(); + db_module.set_native_fn( + "save_markdown", + move |markdown: RhaiMarkdown| -> Result> { + let result = db_clone.set(&markdown).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(result.1) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "get_markdown", + move |id: i64| -> Result> { + let markdown_id = id_from_i64_to_u32(id)?; + db_clone + .get_by_id(markdown_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Markdown with ID {} not found", markdown_id).into(), + Position::NONE, + )) + }) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "delete_markdown", + move |id: i64| -> Result<(), Box> { + let markdown_id = id_from_i64_to_u32(id)?; + db_clone + .collection::() + .unwrap() + .delete_by_id(markdown_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(()) + }, + ); + + // --- Book DB Functions --- + let db_clone = db.clone(); + db_module.set_native_fn( + "save_book", + move |book: RhaiBook| -> Result> { + let result = db_clone.set(&book).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(result.1) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "get_book", + move |id: i64| -> Result> { + let book_id = id_from_i64_to_u32(id)?; + db_clone + .get_by_id(book_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Book with ID {} not found", book_id).into(), + Position::NONE, + )) + }) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "delete_book", + move |id: i64| -> Result<(), Box> { + let book_id = id_from_i64_to_u32(id)?; + db_clone + .collection::() + .unwrap() + .delete_by_id(book_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(()) + }, + ); + + // --- Slides DB Functions --- + let db_clone = db.clone(); + db_module.set_native_fn( + "save_slides", + move |slides: RhaiSlides| -> Result> { + let result = db_clone.set(&slides).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(result.1) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "get_slides", + move |id: i64| -> Result> { + let slides_id = id_from_i64_to_u32(id)?; + db_clone + .get_by_id(slides_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Slides with ID {} not found", slides_id).into(), + Position::NONE, + )) + }) + }, + ); + + let db_clone = db.clone(); + db_module.set_native_fn( + "delete_slides", + move |id: i64| -> Result<(), Box> { + let slides_id = id_from_i64_to_u32(id)?; + db_clone + .collection::() + .unwrap() + .delete_by_id(slides_id) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("DB Error: {:?}", e).into(), + Position::NONE, + )) + })?; + Ok(()) + }, + ); + + engine.register_global_module(db_module.into()); +} diff --git a/src/engine/src/mock_db.rs b/src/engine/src/mock_db.rs index 11b458b..1c1a913 100644 --- a/src/engine/src/mock_db.rs +++ b/src/engine/src/mock_db.rs @@ -65,7 +65,7 @@ fn seed_calendar_data(db: Arc) { calendar.description = Some("My work schedule".to_string()); // Store the calendar in the database - let (calendar_id, updated_calendar) = db + let (_calendar_id, _updated_calendar) = db .collection::() .expect("Failed to get Calendar collection") .set(&calendar) @@ -107,7 +107,7 @@ fn seed_calendar_data(db: Arc) { calendar = calendar.add_event(event_id as i64); // Store the calendar in the database - let (calendar_id, updated_calendar) = db + let (_calendar_id, updated_calendar) = db .collection::() .expect("Failed to get Calendar collection") .set(&calendar) @@ -348,7 +348,7 @@ fn seed_finance_data(db: Arc) { .add_tag("collectible".to_string()); // Store the listing in the database - let (listing_id, updated_listing) = db + let (_listing_id, updated_listing) = db .collection::() .expect("Failed to get Listing collection") .set(&listing)