use std::{
collections::{HashMap, HashSet, VecDeque},
sync::Arc,
};
use chrono::Utc;
use hyphae::{Cell, CellImmutable};
use myko_macros::{myko_report, myko_report_output};
use serde_json::Value;
use crate::{
common::to_value::ToValue,
relationship::{Relation, iter_relations},
store::StoreRegistry,
};
#[myko_report_output]
pub struct ExportedEntity {
pub entity_type: Arc<str>,
pub data: Value,
}
#[myko_report_output]
pub struct EntityTreeExport {
pub version: u32,
pub root_type: Arc<str>,
pub root_id: Arc<str>,
pub exported_at: String,
pub entities: Vec<ExportedEntity>,
}
#[myko_report(EntityTreeExport)]
pub struct ExportEntityTree {
pub root_type: Arc<str>,
pub root_id: Arc<str>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub as_of: Option<Arc<str>>,
}
pub struct ChildRelation {
pub child_type: &'static str,
pub kind: ChildKind,
}
pub enum ChildKind {
BelongsTo {
extract_fk: crate::relationship::FkExtractor,
},
OwnsMany {
extract_ids: crate::relationship::ArrayExtractor,
},
EnsureFor {
extract_fk: crate::relationship::FkExtractor,
},
}
pub fn build_adjacency_map() -> HashMap<&'static str, Vec<ChildRelation>> {
let mut map: HashMap<&'static str, Vec<ChildRelation>> = HashMap::new();
for reg in iter_relations() {
match ®.relation {
Relation::BelongsTo {
local_type,
foreign_type,
extract_fk,
exclude_from_tree,
..
} => {
if *exclude_from_tree {
continue;
}
map.entry(foreign_type).or_default().push(ChildRelation {
child_type: local_type,
kind: ChildKind::BelongsTo {
extract_fk: *extract_fk,
},
});
}
Relation::OwnsMany {
local_type,
foreign_type,
extract_ids,
exclude_from_tree,
..
} => {
if *exclude_from_tree {
continue;
}
map.entry(local_type).or_default().push(ChildRelation {
child_type: foreign_type,
kind: ChildKind::OwnsMany {
extract_ids: *extract_ids,
},
});
}
Relation::EnsureFor {
local_type,
dependencies,
exclude_from_tree,
..
} => {
if *exclude_from_tree {
continue;
}
for dep in *dependencies {
map.entry(dep.foreign_type)
.or_default()
.push(ChildRelation {
child_type: local_type,
kind: ChildKind::EnsureFor {
extract_fk: dep.extract_fk,
},
});
}
}
}
}
map
}
pub fn walk_tree(
root_type: &str,
root_id: &str,
registry: &StoreRegistry,
adjacency: &HashMap<&'static str, Vec<ChildRelation>>,
) -> Vec<ExportedEntity> {
let mut result = Vec::new();
let mut visited: HashSet<(Arc<str>, Arc<str>)> = HashSet::new();
let mut queue: VecDeque<(Arc<str>, Arc<str>)> = VecDeque::new();
let root_type: Arc<str> = root_type.into();
let root_id: Arc<str> = root_id.into();
queue.push_back((root_type.clone(), root_id.clone()));
visited.insert((root_type, root_id));
while let Some((entity_type, entity_id)) = queue.pop_front() {
let Some(store) = registry.get(&entity_type) else {
continue;
};
let Some(entity) = store.get_value(&entity_id) else {
continue;
};
result.push(ExportedEntity {
entity_type: entity_type.clone(),
data: entity.to_value(),
});
let Some(children) = adjacency.get(entity_type.as_ref()) else {
continue;
};
for child_rel in children {
match &child_rel.kind {
ChildKind::BelongsTo { extract_fk } => {
let Some(child_store) = registry.get(child_rel.child_type) else {
continue;
};
for (child_id, child_item) in child_store.snapshot() {
if let Some(fk) = extract_fk(child_item.as_any())
&& fk == entity_id
{
let key = (Arc::<str>::from(child_rel.child_type), child_id);
if visited.insert(key.clone()) {
queue.push_back(key);
}
}
}
}
ChildKind::OwnsMany { extract_ids } => {
if let Some(ids) = extract_ids(entity.as_any()) {
for child_id in ids {
let key = (Arc::<str>::from(child_rel.child_type), child_id);
if visited.insert(key.clone()) {
queue.push_back(key);
}
}
}
}
ChildKind::EnsureFor { extract_fk } => {
let Some(ensured_store) = registry.get(child_rel.child_type) else {
continue;
};
for (ensured_id, ensured_item) in ensured_store.snapshot() {
if let Some(fk) = extract_fk(ensured_item.as_any())
&& fk == entity_id
{
let key = (Arc::<str>::from(child_rel.child_type), ensured_id);
if visited.insert(key.clone()) {
queue.push_back(key);
}
}
}
}
}
}
}
result
}
#[cfg(not(target_arch = "wasm32"))]
impl crate::report::ReportHandler for ExportEntityTree {
type Output = EntityTreeExport;
fn compute(&self, ctx: crate::report::ReportContext) -> Cell<Arc<Self::Output>, CellImmutable> {
let registry = if let Some(as_of) = &self.as_of {
match ctx.replay_store(as_of) {
Ok(r) => r,
Err(err) => {
eprintln!(
"[ExportEntityTree] replay_store FAILED: as_of={} err={}",
as_of, err
);
return Cell::new(Arc::new(EntityTreeExport {
version: 1,
root_type: self.root_type.clone(),
root_id: self.root_id.clone(),
exported_at: Utc::now().to_rfc3339(),
entities: vec![],
}))
.lock();
}
}
} else {
ctx.registry()
};
eprintln!(
"[ExportEntityTree] registry has {} entity types, walking root_type={} root_id={}",
registry.entity_types().len(),
self.root_type,
self.root_id,
);
let adjacency = build_adjacency_map();
let entities = walk_tree(&self.root_type, &self.root_id, ®istry, &adjacency);
eprintln!(
"[ExportEntityTree] walk_tree found {} entities",
entities.len()
);
Cell::new(Arc::new(EntityTreeExport {
version: 1,
root_type: self.root_type.clone(),
root_id: self.root_id.clone(),
exported_at: Utc::now().to_rfc3339(),
entities,
}))
.lock()
}
}