Skip to main content

myko/core/report/
export_tree.rs

1//! Entity tree export report.
2//!
3//! BFS walks the relationship graph from a root entity, collecting all
4//! descendant entities into a flat `EntityTreeExport` structure.
5
6use std::{
7    collections::{HashMap, HashSet, VecDeque},
8    sync::Arc,
9};
10
11use chrono::Utc;
12use hyphae::{Cell, MaterializeDefinite};
13use myko_macros::{myko_report, myko_report_output};
14use serde_json::Value;
15
16use crate::{
17    common::to_value::ToValue,
18    relationship::{Relation, iter_relations},
19    store::StoreRegistry,
20};
21
22// ─────────────────────────────────────────────────────────────────────────────
23// Output types
24// ─────────────────────────────────────────────────────────────────────────────
25
26/// A single exported entity within the tree.
27#[myko_report_output]
28pub struct ExportedEntity {
29    /// The entity type name (e.g., "Scene", "Binding").
30    pub entity_type: Arc<str>,
31    /// The full serialized entity data.
32    pub data: Value,
33}
34
35/// The complete tree export containing all entities reachable from the root.
36#[myko_report_output]
37pub struct EntityTreeExport {
38    /// Export format version.
39    pub version: u32,
40    /// Entity type of the root (e.g., "Project").
41    pub root_type: Arc<str>,
42    /// ID of the root entity.
43    pub root_id: Arc<str>,
44    /// ISO 8601 timestamp of when the export was created.
45    pub exported_at: String,
46    /// All entities in the tree, flattened.
47    pub entities: Vec<ExportedEntity>,
48}
49
50// ─────────────────────────────────────────────────────────────────────────────
51// Report definition
52// ─────────────────────────────────────────────────────────────────────────────
53
54/// Export the full entity tree rooted at a given entity.
55///
56/// Performs a BFS walk over the relationship graph, collecting every
57/// reachable descendant. Respects `exclude_from_tree` on `BelongsTo`
58/// relations and follows `EnsureFor` on a single-axis basis to prevent
59/// Cartesian explosion.
60#[myko_report(EntityTreeExport)]
61pub struct ExportEntityTree {
62    /// Entity type of the root (e.g., "Project").
63    pub root_type: Arc<str>,
64    /// ID of the root entity.
65    pub root_id: Arc<str>,
66    /// ISO 8601 timestamp — when set, replays events up to this time
67    /// into a temporary store and exports from that instead of the live store.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    #[ts(optional = nullable)]
70    pub as_of: Option<Arc<str>>,
71}
72
73// ─────────────────────────────────────────────────────────────────────────────
74// Adjacency map
75// ─────────────────────────────────────────────────────────────────────────────
76
77/// Describes how to find children of a given parent entity type.
78pub struct ChildRelation {
79    /// The child entity type.
80    pub child_type: &'static str,
81    /// How to discover child IDs from the parent.
82    pub kind: ChildKind,
83}
84
85pub enum ChildKind {
86    /// BelongsTo: scan the child store for entities whose FK matches the parent ID.
87    BelongsTo {
88        extract_fk: crate::relationship::FkExtractor,
89    },
90    /// OwnsMany: extract child IDs directly from the parent entity.
91    OwnsMany {
92        extract_ids: crate::relationship::ArrayExtractor,
93    },
94    /// EnsureFor: scan the local (ensured) store for entities whose FK matches the parent ID.
95    EnsureFor {
96        extract_fk: crate::relationship::FkExtractor,
97    },
98}
99
100/// Build a map from parent entity type -> list of child relations.
101///
102/// This processes all registered relationships once and inverts them into
103/// a lookup table suitable for BFS traversal.
104pub fn build_adjacency_map() -> HashMap<&'static str, Vec<ChildRelation>> {
105    let mut map: HashMap<&'static str, Vec<ChildRelation>> = HashMap::new();
106
107    for reg in iter_relations() {
108        match &reg.relation {
109            Relation::BelongsTo {
110                local_type,
111                foreign_type,
112                extract_fk,
113                exclude_from_tree,
114                ..
115            } => {
116                if *exclude_from_tree {
117                    continue;
118                }
119                // Parent is foreign_type, child is local_type.
120                map.entry(foreign_type).or_default().push(ChildRelation {
121                    child_type: local_type,
122                    kind: ChildKind::BelongsTo {
123                        extract_fk: *extract_fk,
124                    },
125                });
126            }
127            Relation::OwnsMany {
128                local_type,
129                foreign_type,
130                extract_ids,
131                exclude_from_tree,
132                ..
133            } => {
134                if *exclude_from_tree {
135                    continue;
136                }
137                // Parent is local_type, child is foreign_type.
138                map.entry(local_type).or_default().push(ChildRelation {
139                    child_type: foreign_type,
140                    kind: ChildKind::OwnsMany {
141                        extract_ids: *extract_ids,
142                    },
143                });
144            }
145            Relation::EnsureFor {
146                local_type,
147                dependencies,
148                exclude_from_tree,
149                ..
150            } => {
151                if *exclude_from_tree {
152                    continue;
153                }
154                // For each dependency, the dependency's foreign_type is a parent
155                // that can reach local_type children (single-axis to avoid Cartesian).
156                for dep in *dependencies {
157                    map.entry(dep.foreign_type)
158                        .or_default()
159                        .push(ChildRelation {
160                            child_type: local_type,
161                            kind: ChildKind::EnsureFor {
162                                extract_fk: dep.extract_fk,
163                            },
164                        });
165                }
166            }
167        }
168    }
169
170    map
171}
172
173// ─────────────────────────────────────────────────────────────────────────────
174// BFS walk
175// ─────────────────────────────────────────────────────────────────────────────
176
177/// Walk the entity tree via BFS starting from `(root_type, root_id)`.
178///
179/// Returns all reachable entities (including the root) as `ExportedEntity` values.
180pub fn walk_tree(
181    root_type: &str,
182    root_id: &str,
183    registry: &StoreRegistry,
184    adjacency: &HashMap<&'static str, Vec<ChildRelation>>,
185) -> Vec<ExportedEntity> {
186    let mut result = Vec::new();
187    let mut visited: HashSet<(Arc<str>, Arc<str>)> = HashSet::new();
188    let mut queue: VecDeque<(Arc<str>, Arc<str>)> = VecDeque::new();
189
190    let root_type: Arc<str> = root_type.into();
191    let root_id: Arc<str> = root_id.into();
192
193    queue.push_back((root_type.clone(), root_id.clone()));
194    visited.insert((root_type, root_id));
195
196    while let Some((entity_type, entity_id)) = queue.pop_front() {
197        // Fetch entity from store
198        let Some(store) = registry.get(&entity_type) else {
199            continue;
200        };
201        let Some(entity) = store.get_value(&entity_id) else {
202            continue;
203        };
204
205        // Serialize entity
206        result.push(ExportedEntity {
207            entity_type: entity_type.clone(),
208            data: entity.to_value(),
209        });
210
211        // Find children via adjacency map
212        let Some(children) = adjacency.get(entity_type.as_ref()) else {
213            continue;
214        };
215
216        for child_rel in children {
217            match &child_rel.kind {
218                ChildKind::BelongsTo { extract_fk } => {
219                    // Scan child store for entities whose FK matches this entity's ID
220                    let Some(child_store) = registry.get(child_rel.child_type) else {
221                        continue;
222                    };
223                    for (child_id, child_item) in child_store.snapshot() {
224                        if let Some(fk) = extract_fk(child_item.as_any())
225                            && fk == entity_id
226                        {
227                            let key = (Arc::<str>::from(child_rel.child_type), child_id);
228                            if visited.insert(key.clone()) {
229                                queue.push_back(key);
230                            }
231                        }
232                    }
233                }
234                ChildKind::OwnsMany { extract_ids } => {
235                    // Extract child IDs directly from the parent entity
236                    if let Some(ids) = extract_ids(entity.as_any()) {
237                        for child_id in ids {
238                            let key = (Arc::<str>::from(child_rel.child_type), child_id);
239                            if visited.insert(key.clone()) {
240                                queue.push_back(key);
241                            }
242                        }
243                    }
244                }
245                ChildKind::EnsureFor { extract_fk } => {
246                    // Scan the ensured entity store for entities whose FK matches this entity's ID
247                    let Some(ensured_store) = registry.get(child_rel.child_type) else {
248                        continue;
249                    };
250                    for (ensured_id, ensured_item) in ensured_store.snapshot() {
251                        if let Some(fk) = extract_fk(ensured_item.as_any())
252                            && fk == entity_id
253                        {
254                            let key = (Arc::<str>::from(child_rel.child_type), ensured_id);
255                            if visited.insert(key.clone()) {
256                                queue.push_back(key);
257                            }
258                        }
259                    }
260                }
261            }
262        }
263    }
264
265    result
266}
267
268// ─────────────────────────────────────────────────────────────────────────────
269// ReportHandler impl
270// ─────────────────────────────────────────────────────────────────────────────
271
272#[cfg(not(target_arch = "wasm32"))]
273impl crate::report::ReportHandler for ExportEntityTree {
274    type Output = EntityTreeExport;
275
276    fn compute(
277        &self,
278        ctx: crate::report::ReportContext,
279    ) -> impl MaterializeDefinite<Arc<Self::Output>> {
280        let registry = if let Some(as_of) = &self.as_of {
281            match ctx.replay_store(as_of) {
282                Ok(r) => r,
283                Err(err) => {
284                    eprintln!(
285                        "[ExportEntityTree] replay_store FAILED: as_of={} err={}",
286                        as_of, err
287                    );
288                    return Cell::new(Arc::new(EntityTreeExport {
289                        version: 1,
290                        root_type: self.root_type.clone(),
291                        root_id: self.root_id.clone(),
292                        exported_at: Utc::now().to_rfc3339(),
293                        entities: vec![],
294                    }))
295                    .lock();
296                }
297            }
298        } else {
299            ctx.registry()
300        };
301
302        eprintln!(
303            "[ExportEntityTree] registry has {} entity types, walking root_type={} root_id={}",
304            registry.entity_types().len(),
305            self.root_type,
306            self.root_id,
307        );
308        let adjacency = build_adjacency_map();
309        let entities = walk_tree(&self.root_type, &self.root_id, &registry, &adjacency);
310        eprintln!(
311            "[ExportEntityTree] walk_tree found {} entities",
312            entities.len()
313        );
314
315        Cell::new(Arc::new(EntityTreeExport {
316            version: 1,
317            root_type: self.root_type.clone(),
318            root_id: self.root_id.clone(),
319            exported_at: Utc::now().to_rfc3339(),
320            entities,
321        }))
322        .lock()
323    }
324}