headless_lms_models/library/agent_changesets/
reverting.rs1use std::collections::HashMap;
4
5use crate::{
6 agent_changeset_calls, agent_changesets,
7 library::{
8 agent_changesets::course_state::{CourseView, StagedEntity},
9 entity_snapshots::{ContentEntityType, EntityKey, EntityRef, EntitySnapshot},
10 },
11 prelude::*,
12};
13
14#[derive(Debug, Clone)]
16pub enum RevertPlan {
17 Ready {
19 tool_name: String,
21 entities: Vec<StagedEntity>,
22 current: HashMap<EntityKey, Option<EntitySnapshot>>,
24 },
25 Conflicts(Vec<EntityRef>),
27}
28
29pub async fn plan_revert(
37 conn: &mut PgConnection,
38 view: &CourseView,
39 call_id: Uuid,
40 user_id: Uuid,
41) -> ModelResult<RevertPlan> {
42 let call = agent_changeset_calls::get_by_id(conn, call_id).await?;
43 let changeset = agent_changesets::get_by_id_for_user(conn, call.changeset_id, user_id).await?;
44 if changeset.course_id != view.course.id {
45 return Err(model_err!(NotFound, "No such call".to_string()));
46 }
47 let page_ids: Vec<Uuid> = call
48 .entity_changes
49 .iter()
50 .filter(|change| change.entity_type == ContentEntityType::Page)
51 .map(|change| change.entity_id)
52 .collect();
53 let mut current_pages = view.current_page_snapshots(conn, &page_ids).await?;
54 let mut current_states: HashMap<EntityKey, Option<EntitySnapshot>> = HashMap::new();
55 for change in &call.entity_changes {
56 let key = EntityKey::new(change.entity_type, change.entity_id);
57 let current = match key.entity_type {
58 ContentEntityType::Page => current_pages
59 .remove(&key.entity_id)
60 .map(EntitySnapshot::Page),
61 _ => view.staged.entity_snapshot(key),
62 };
63 current_states.entry(key).or_insert(current);
64 }
65
66 let mut conflicts = Vec::new();
67 let mut entities = Vec::with_capacity(call.entity_changes.len());
68 for change in call.entity_changes {
69 let key = EntityKey::new(change.entity_type, change.entity_id);
70 let current = current_states.get(&key).and_then(Option::as_ref);
71 let current_digest = current.map(EntitySnapshot::digest).transpose()?;
72 if current_digest != change.after_digest {
73 conflicts.push(EntityRef {
74 entity_type: change.entity_type,
75 entity_id: change.entity_id,
76 label: current.map_or(change.label, |current| current.label().to_string()),
77 });
78 continue;
79 }
80 entities.push(StagedEntity::new(key, change.before));
81 }
82 if conflicts.is_empty() {
83 Ok(RevertPlan::Ready {
84 tool_name: call.tool_name,
85 entities,
86 current: current_states,
87 })
88 } else {
89 Ok(RevertPlan::Conflicts(conflicts))
90 }
91}