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 ®.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, ®istry, &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}