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