update grid4 & heroledger models

This commit is contained in:
Timur Gordon
2025-09-16 14:18:08 +02:00
parent cb1fb0f0ec
commit 53e9a2d4f0
31 changed files with 3216 additions and 399 deletions

View File

@@ -0,0 +1,117 @@
use serde_json;
use heromodels::models::grid4::{
ComputeSlice, DeviceInfo, Node, NodeCapacity, PricingPolicy, Reservation, ReservationStatus,
SLAPolicy, StorageDevice, StorageSlice,
};
#[test]
fn build_and_serde_roundtrip_compute_storage_slices() {
let pricing = PricingPolicy::new()
.marketplace_year_discounts(vec![20, 30, 40])
.volume_discounts(vec![5, 10, 15])
.build();
let sla = SLAPolicy::new()
.sla_uptime(99)
.sla_bandwidth_mbit(1000)
.sla_penalty(150)
.build();
let cs = ComputeSlice::new()
.nodeid(42)
.slice_id(1)
.mem_gb(16.0)
.storage_gb(200.0)
.passmark(5000)
.vcores(8)
.cpu_oversubscription(2)
.storage_oversubscription(1)
.price_range(vec![0.5, 2.0])
.gpus(1)
.price_cc(1.25)
.pricing_policy(pricing.clone())
.sla_policy(sla.clone());
let ss = StorageSlice::new()
.nodeid(42)
.slice_id(2)
.price_cc(0.15)
.pricing_policy(pricing)
.sla_policy(sla);
// serde roundtrip compute slice
let s = serde_json::to_string(&cs).expect("serialize compute slice");
let cs2: ComputeSlice = serde_json::from_str(&s).expect("deserialize compute slice");
assert_eq!(cs, cs2);
// serde roundtrip storage slice
let s2 = serde_json::to_string(&ss).expect("serialize storage slice");
let ss2: StorageSlice = serde_json::from_str(&s2).expect("deserialize storage slice");
assert_eq!(ss, ss2);
}
#[test]
fn build_and_serde_roundtrip_node() {
let dev = DeviceInfo {
vendor: "AcmeVendor".into(),
storage: vec![StorageDevice { id: "sda".into(), size_gb: 512.0, description: "NVMe".into() }],
memory: vec![],
cpu: vec![],
gpu: vec![],
network: vec![],
};
let cap = NodeCapacity { storage_gb: 2048.0, mem_gb: 128.0, mem_gb_gpu: 24.0, passmark: 12000, vcores: 32 };
let cs = ComputeSlice::new().nodeid(1).slice_id(1).mem_gb(8.0).storage_gb(100.0).passmark(2500).vcores(4);
let ss = StorageSlice::new().nodeid(1).slice_id(2).price_cc(0.2);
let node = Node::new()
.nodegroupid(7)
.uptime(99)
.add_compute_slice(cs)
.add_storage_slice(ss)
.devices(dev)
.country("NL")
.capacity(cap)
.provisiontime(1710000000)
.pubkey("node_pubkey")
.signature_node("sig_node")
.signature_farmer("sig_farmer");
let s = serde_json::to_string(&node).expect("serialize node");
let node2: Node = serde_json::from_str(&s).expect("deserialize node");
assert_eq!(node.nodegroupid, node2.nodegroupid);
assert_eq!(node.uptime, node2.uptime);
assert_eq!(node.country, node2.country);
assert_eq!(node.pubkey, node2.pubkey);
assert_eq!(node.signature_node, node2.signature_node);
assert_eq!(node.signature_farmer, node2.signature_farmer);
assert_eq!(node.computeslices.len(), node2.computeslices.len());
assert_eq!(node.storageslices.len(), node2.storageslices.len());
}
#[test]
fn build_and_serde_roundtrip_reservation() {
let reservation = Reservation::new()
.customer_id(1234)
.add_compute_slice(11)
.add_storage_slice(22)
.status(ReservationStatus::Confirmed)
.obligation(true)
.start_date(1_710_000_000)
.end_date(1_720_000_000);
let s = serde_json::to_string(&reservation).expect("serialize reservation");
let reservation2: Reservation = serde_json::from_str(&s).expect("deserialize reservation");
assert_eq!(reservation.customer_id, reservation2.customer_id);
assert_eq!(reservation.status, reservation2.status);
assert_eq!(reservation.obligation, reservation2.obligation);
assert_eq!(reservation.start_date, reservation2.start_date);
assert_eq!(reservation.end_date, reservation2.end_date);
assert_eq!(reservation.compute_slices, reservation2.compute_slices);
assert_eq!(reservation.storage_slices, reservation2.storage_slices);
}

View File

@@ -0,0 +1,82 @@
use heromodels::db::hero::OurDB;
use heromodels::db::{Collection, Db};
use heromodels::models::grid4::node::node_index::{country, nodegroupid, pubkey};
use heromodels::models::grid4::node::{ComputeSlice, DeviceInfo, Node};
use heromodels_core::Model;
use std::sync::Arc;
fn create_test_db() -> Arc<OurDB> {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = format!("/tmp/grid4_node_test_{}", ts);
let _ = std::fs::remove_dir_all(&path);
Arc::new(OurDB::new(path, true).expect("create OurDB"))
}
#[test]
fn grid4_node_basic_roundtrip_and_indexes() {
let db = create_test_db();
let nodes = db.collection::<Node>().expect("open node collection");
// Clean any leftover
if let Ok(existing) = nodes.get_all() {
for n in existing {
let _ = nodes.delete_by_id(n.get_id());
}
}
// Build a node with some compute slices and device info
let cs = ComputeSlice::new()
.nodeid(1)
.slice_id(1)
.mem_gb(32.0)
.storage_gb(512.0)
.passmark(5000)
.vcores(16)
.gpus(1)
.price_cc(0.25);
let dev = DeviceInfo {
vendor: "ACME".into(),
..Default::default()
};
let n = Node::new()
.nodegroupid(42)
.uptime(99)
.add_compute_slice(cs)
.devices(dev)
.country("BE")
.pubkey("PUB_NODE_1")
.build();
let (id, stored) = nodes.set(&n).expect("store node");
assert!(id > 0);
assert_eq!(stored.country, "BE");
// get by id
let fetched = nodes.get_by_id(id).expect("get by id").expect("exists");
assert_eq!(fetched.pubkey, "PUB_NODE_1");
// query by top-level indexes
let by_country = nodes.get::<country, _>("BE").expect("query country");
assert_eq!(by_country.len(), 1);
assert_eq!(by_country[0].get_id(), id);
let by_group = nodes.get::<nodegroupid, _>(&42).expect("query group");
assert_eq!(by_group.len(), 1);
let by_pubkey = nodes.get::<pubkey, _>("PUB_NODE_1").expect("query pubkey");
assert_eq!(by_pubkey.len(), 1);
// update
let updated = fetched.clone().country("NL");
let (_, back) = nodes.set(&updated).expect("update node");
assert_eq!(back.country, "NL");
// delete
nodes.delete_by_id(id).expect("delete");
assert!(nodes.get_by_id(id).expect("get after delete").is_none());
}

View File

@@ -0,0 +1,125 @@
use heromodels::db::postgres::{Config, Postgres};
use heromodels::db::{Collection, Db};
use heromodels::models::grid4::node::node_index::{country, nodegroupid, pubkey};
use heromodels::models::grid4::node::{ComputeSlice, DeviceInfo, Node};
use heromodels_core::Model;
// Requires local Postgres (user=postgres password=test123 host=localhost port=5432)
// Run with: cargo test -p heromodels --test grid4_postgres -- --ignored
#[test]
#[ignore]
fn grid4_node_postgres_roundtrip_like_example() {
let db = Postgres::new(
Config::new()
.user(Some("postgres".into()))
.password(Some("test123".into()))
.host(Some("localhost".into()))
.port(Some(5432)),
)
.expect("can connect to Postgres");
let nodes = db.collection::<Node>().expect("open node collection");
// Clean existing
if let Ok(existing) = nodes.get_all() {
for n in existing {
let _ = nodes.delete_by_id(n.get_id());
}
}
// Build and store multiple nodes via builder and then persist via collection.set(), like examples
let cs1 = ComputeSlice::new()
.nodeid(10)
.slice_id(1)
.mem_gb(32.0)
.storage_gb(512.0)
.passmark(5000)
.vcores(16)
.gpus(1)
.price_cc(0.25);
let cs2 = ComputeSlice::new()
.nodeid(10)
.slice_id(2)
.mem_gb(64.0)
.storage_gb(2048.0)
.passmark(7000)
.vcores(24)
.gpus(2)
.price_cc(0.50);
let cs3 = ComputeSlice::new()
.nodeid(11)
.slice_id(1)
.mem_gb(16.0)
.storage_gb(256.0)
.passmark(3000)
.vcores(8)
.gpus(0)
.price_cc(0.10);
let dev = DeviceInfo { vendor: "ACME".into(), ..Default::default() };
let n1 = Node::new()
.nodegroupid(99)
.uptime(97)
.add_compute_slice(cs1)
.devices(dev.clone())
.country("BE")
.pubkey("PG_NODE_1")
.build();
let n2 = Node::new()
.nodegroupid(99)
.uptime(96)
.add_compute_slice(cs2)
.devices(dev.clone())
.country("NL")
.pubkey("PG_NODE_2")
.build();
let n3 = Node::new()
.nodegroupid(7)
.uptime(95)
.add_compute_slice(cs3)
.devices(dev)
.country("BE")
.pubkey("PG_NODE_3")
.build();
let (id1, s1) = nodes.set(&n1).expect("store n1");
let (id2, s2) = nodes.set(&n2).expect("store n2");
let (id3, s3) = nodes.set(&n3).expect("store n3");
assert!(id1 > 0 && id2 > 0 && id3 > 0);
// Query by top-level indexes similar to the example style
let be_nodes = nodes.get::<country, _>("BE").expect("by country");
assert_eq!(be_nodes.len(), 2);
let grp_99 = nodes.get::<nodegroupid, _>(&99).expect("by group");
assert_eq!(grp_99.len(), 2);
let by_key = nodes.get::<pubkey, _>("PG_NODE_2").expect("by pubkey");
assert_eq!(by_key.len(), 1);
assert_eq!(by_key[0].get_id(), id2);
// Update: change country of n1
let updated = s1.clone().country("DE");
let (_, back) = nodes.set(&updated).expect("update n1");
assert_eq!(back.country, "DE");
// Cardinality after update
let de_nodes = nodes.get::<country, _>("DE").expect("by country DE");
assert_eq!(de_nodes.len(), 1);
// Delete by id and by index
nodes.delete_by_id(id2).expect("delete n2 by id");
assert!(nodes.get_by_id(id2).unwrap().is_none());
nodes.delete::<pubkey, _>("PG_NODE_3").expect("delete n3 by pubkey");
assert!(nodes.get_by_id(id3).unwrap().is_none());
// Remaining should be updated n1 only; verify via targeted queries
let de_nodes = nodes.get::<country, _>("DE").expect("country DE after deletes");
assert_eq!(de_nodes.len(), 1);
assert_eq!(de_nodes[0].get_id(), id1);
let by_key = nodes.get::<pubkey, _>("PG_NODE_1").expect("by pubkey PG_NODE_1");
assert_eq!(by_key.len(), 1);
assert_eq!(by_key[0].get_id(), id1);
}

View File

@@ -0,0 +1,97 @@
use heromodels::db::postgres::{Config, Postgres};
use heromodels::db::{Collection, Db};
use heromodels::models::heroledger::user::user_index::username;
use heromodels::models::heroledger::user::User;
use heromodels_core::Model;
// NOTE: Requires a local Postgres running with user=postgres password=test123 host=localhost port=5432
// Marked ignored by default. Run with: cargo test -p heromodels --test heroledger_postgres -- --ignored
#[test]
#[ignore]
fn heroledger_user_postgres_roundtrip() {
// Connect
let db = Postgres::new(
Config::new()
.user(Some("postgres".into()))
.password(Some("test123".into()))
.host(Some("localhost".into()))
.port(Some(5432)),
)
.expect("can connect to Postgres");
// Open collection (will create table and indexes for top-level fields)
let users = db.collection::<User>().expect("can open user collection");
// Clean slate
if let Ok(existing) = users.get_all() {
for u in existing {
let _ = users.delete_by_id(u.get_id());
}
}
// Unique suffix to avoid collisions with any pre-existing rows
let uniq = format!("{}", std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos());
let alice = format!("alice_{}", uniq);
let bob = format!("bob_{}", uniq);
let carol = format!("carol_{}", uniq);
// Build and store multiple users
let u1 = User::new(0)
.username(&alice)
.pubkey("PUBKEY_A")
.add_email("alice@example.com")
.build();
let u2 = User::new(0)
.username(&bob)
.pubkey("PUBKEY_B")
.add_email("bob@example.com")
.build();
let u3 = User::new(0)
.username(&carol)
.pubkey("PUBKEY_C")
.add_email("carol@example.com")
.build();
let (id1, db_u1) = users.set(&u1).expect("store u1");
let (id2, db_u2) = users.set(&u2).expect("store u2");
let (id3, db_u3) = users.set(&u3).expect("store u3");
assert!(id1 > 0 && id2 > 0 && id3 > 0);
// Fetch by id
assert_eq!(users.get_by_id(id1).unwrap().unwrap().username, alice);
assert_eq!(users.get_by_id(id2).unwrap().unwrap().username, bob);
assert_eq!(users.get_by_id(id3).unwrap().unwrap().username, carol);
// Fetch by index (top-level username)
let by_username = users.get::<username, _>(&alice).expect("by username");
assert_eq!(by_username.len(), 1);
assert_eq!(by_username[0].get_id(), id1);
// Update one
let updated = db_u1.clone().add_email("work@alice.example");
let (id1b, updated_back) = users.set(&updated).expect("update alice");
assert_eq!(id1b, id1);
assert!(updated_back.email.len() >= 2);
// Targeted queries to avoid legacy rows in the same table
// Verify three users exist via index queries
assert_eq!(users.get::<username, _>(&alice).unwrap().len(), 1);
assert_eq!(users.get::<username, _>(&bob).unwrap().len(), 1);
assert_eq!(users.get::<username, _>(&carol).unwrap().len(), 1);
// Delete by id
users.delete_by_id(id2).expect("delete bob by id");
assert!(users.get_by_id(id2).unwrap().is_none());
// Delete by index (username)
users.delete::<username, _>(&carol).expect("delete carol by username");
assert!(users.get_by_id(id3).unwrap().is_none());
// Remaining should be just alice; verify via index
let remain = users.get::<username, _>(&alice).expect("get alice after delete");
assert_eq!(remain.len(), 1);
assert_eq!(remain[0].get_id(), id1);
}

View File

@@ -1,4 +1,5 @@
use heromodels::db::Collection;
use heromodels::db::Db;
use heromodels::db::hero::OurDB;
use heromodels::models::biz::{BusinessType, Company, CompanyStatus, Payment, PaymentStatus};
use heromodels_core::Model;
@@ -197,12 +198,18 @@ fn test_payment_database_persistence() {
);
// Save payment
let (payment_id, saved_payment) = db.set(&payment).expect("Failed to save payment");
let (payment_id, saved_payment) = db
.collection::<Payment>()
.expect("open payment collection")
.set(&payment)
.expect("Failed to save payment");
assert!(payment_id > 0);
assert_eq!(saved_payment.payment_intent_id, "pi_db_test");
// Retrieve payment
let retrieved_payment: Payment = db
.collection::<Payment>()
.expect("open payment collection")
.get_by_id(payment_id)
.expect("Failed to get payment")
.unwrap();
@@ -224,20 +231,34 @@ fn test_payment_status_transitions() {
1360.0,
);
let (payment_id, mut payment) = db.set(&payment).expect("Failed to save payment");
let (payment_id, mut payment) = db
.collection::<Payment>()
.expect("open payment collection")
.set(&payment)
.expect("Failed to save payment");
// Test pending -> completed
payment = payment.complete_payment(Some("cus_transition_test".to_string()));
let (_, mut payment) = db.set(&payment).expect("Failed to update payment");
let (_, mut payment) = db
.collection::<Payment>()
.expect("open payment collection")
.set(&payment)
.expect("Failed to update payment");
assert!(payment.is_completed());
// Test completed -> refunded
payment = payment.refund_payment();
let (_, payment) = db.set(&payment).expect("Failed to update payment");
let (_, payment) = db
.collection::<Payment>()
.expect("open payment collection")
.set(&payment)
.expect("Failed to update payment");
assert!(payment.is_refunded());
// Verify final state in database
let final_payment: Payment = db
.collection::<Payment>()
.expect("open payment collection")
.get_by_id(payment_id)
.expect("Failed to get payment")
.unwrap();
@@ -270,15 +291,18 @@ fn test_company_payment_integration() {
let db = create_test_db();
// Create company with default PendingPayment status
let company = Company::new(
"Integration Test Corp".to_string(),
"ITC-001".to_string(),
chrono::Utc::now().timestamp(),
)
.email("test@integration.com".to_string())
.business_type(BusinessType::Starter);
let company = Company::new()
.name("Integration Test Corp")
.registration_number("ITC-001")
.incorporation_date(chrono::Utc::now().timestamp())
.email("test@integration.com")
.business_type(BusinessType::Starter);
let (company_id, company) = db.set(&company).expect("Failed to save company");
let (company_id, company) = db
.collection::<Company>()
.expect("open company collection")
.set(&company)
.expect("Failed to save company");
assert_eq!(company.status, CompanyStatus::PendingPayment);
// Create payment for the company
@@ -291,18 +315,28 @@ fn test_company_payment_integration() {
305.0,
);
let (_payment_id, payment) = db.set(&payment).expect("Failed to save payment");
let (_payment_id, payment) = db
.collection::<Payment>()
.expect("open payment collection")
.set(&payment)
.expect("Failed to save payment");
assert_eq!(payment.company_id, company_id);
// Complete payment
let completed_payment = payment.complete_payment(Some("cus_integration_test".to_string()));
let (_, completed_payment) = db
.collection::<Payment>()
.expect("open payment collection")
.set(&completed_payment)
.expect("Failed to update payment");
// Update company status to Active
let active_company = company.status(CompanyStatus::Active);
let (_, active_company) = db.set(&active_company).expect("Failed to update company");
let (_, active_company) = db
.collection::<Company>()
.expect("open company collection")
.set(&active_company)
.expect("Failed to update company");
// Verify final states
assert!(completed_payment.is_completed());