evergreen/common/trigger/
mod.rs

1//! Action/Trigger main entry point.
2use crate as eg;
3use eg::common::org;
4use eg::date;
5use eg::idl;
6use eg::Editor;
7use eg::EgResult;
8use eg::EgValue;
9
10pub mod event;
11pub use event::{Event, EventState};
12pub mod processor;
13pub use processor::Processor;
14mod reactor;
15mod validator;
16
17/// Create A/T events for an object and A/T hook.
18pub fn create_events_for_object(
19    editor: &mut Editor,
20    hook: &str,
21    target: &EgValue,
22    org_id: i64,
23    granularity: Option<&str>,
24    user_data: Option<&EgValue>,
25    ignore_opt_in: bool,
26) -> EgResult<()> {
27    let hook_obj = match editor.retrieve("ath", hook)? {
28        Some(h) => h,
29        None => {
30            log::warn!("No such A/T hook: {hook}");
31            return Ok(());
32        }
33    };
34
35    let class = target
36        .classname()
37        .ok_or_else(|| format!("Invalid target: {target}"))?;
38
39    if hook_obj["core_type"].as_str().unwrap() != class {
40        // "key" is required.
41        log::warn!("A/T hook {hook} does not match object core type: {class}");
42        return Ok(());
43    }
44
45    let query = eg::hash! {
46        "hook": hook,
47        "active": "t",
48        "owner": org::ancestors(editor, org_id)?,
49    };
50
51    let event_defs = editor.search("atevdef", query)?;
52
53    for def in event_defs.iter() {
54        create_event_for_object_and_def(
55            editor,
56            def,
57            target,
58            granularity,
59            user_data,
60            ignore_opt_in,
61        )?;
62    }
63
64    Ok(())
65}
66
67/// Take one target and one event def and create an event if we can.
68///
69/// Assumes that the target is appropriate for the event def.
70pub fn create_event_for_object_and_def(
71    editor: &mut Editor,
72    event_def: &EgValue,
73    target: &EgValue,
74    granularity: Option<&str>,
75    user_data: Option<&EgValue>,
76    ignore_opt_in: bool,
77) -> EgResult<Option<EgValue>> {
78    if let Some(gran) = granularity {
79        // If a granularity is provided by the caller, the def
80        // must a) have one and b) have one that matches.
81        if let Some(def_gran) = event_def["granularity"].as_str() {
82            if def_gran != gran {
83                return Ok(None);
84            }
85        } else {
86            return Ok(None);
87        }
88    }
89
90    if !ignore_opt_in && !user_is_opted_in(editor, event_def, target)? {
91        return Ok(None);
92    }
93
94    let runtime = match calc_runtime(event_def, target)? {
95        Some(t) => t,
96        None => return Ok(None),
97    };
98
99    let pkey = target
100        .pkey_value()
101        .ok_or_else(|| "Pkey value required".to_string())?;
102
103    let mut event = eg::hash! {
104        "event_def": event_def["id"].clone(),
105        "run_time": runtime,
106    };
107
108    event["target"] = pkey.clone();
109
110    if let Some(udata) = user_data {
111        event["user_data"] = EgValue::from(udata.dump());
112    }
113
114    event.bless("atev")?;
115
116    Ok(Some(editor.create(event)?))
117}
118
119// Non-doc test required since this is a private function.
120#[test]
121fn test_calc_runtime() {
122    let event_def = eg::hash! {
123      "passive": "t",
124      "delay_field": "due_date",
125      "delay": "1 day 1 hour 5 minutes 1 second",
126    };
127
128    let target = eg::hash! {
129      "due_date": "2023-08-18T23:59:59-0400"
130    };
131
132    let runtime = calc_runtime(&event_def, &target).unwrap();
133    assert_eq!(runtime, Some("2023-08-20T01:05:00-0400".to_string()));
134}
135
136/// Determine the run_time value for an event.
137///
138/// Returns the value as an ISO string.
139fn calc_runtime(event_def: &EgValue, target: &EgValue) -> EgResult<Option<String>> {
140    if !event_def["passive"].boolish() {
141        // Active events always run now.
142        return Ok(Some(date::to_iso(&date::now_local())));
143    }
144
145    let delay_field = match event_def["delay_field"].as_str() {
146        Some(d) => d,
147        None => return Ok(Some(date::to_iso(&date::now_local()))),
148    };
149
150    let delay_start = match target[delay_field].as_str() {
151        Some(a) => a,
152        None => return Ok(None),
153    };
154
155    let delay_intvl = match event_def["delay"].as_str() {
156        Some(d) => d,
157        None => return Ok(None), // required field / should not happen.
158    };
159
160    let runtime = date::parse_datetime(delay_start)?;
161    let runtime = date::add_interval(runtime, delay_intvl)?;
162
163    Ok(Some(date::to_iso(&runtime)))
164}
165
166/// Returns true if the event def does not require opt in (i.e. everyone
167/// is opted in) or it does require an opt-in and the user linked to the
168/// target has the needed opt-in user setting.
169fn user_is_opted_in(editor: &mut Editor, event_def: &EgValue, target: &EgValue) -> EgResult<bool> {
170    let opt_in = match event_def["opt_in_setting"].as_str() {
171        Some(o) => o,
172        None => return Ok(true),
173    };
174
175    // If the event def requires an opt-in but defines no user field,
176    // then no one is opted in.
177    let usr_field = match event_def["usr_field"].as_str() {
178        Some(f) => f,
179        None => return Ok(false),
180    };
181
182    let user_id = target[usr_field]
183        .as_int()
184        .unwrap_or(target[usr_field].id()?);
185
186    let query = eg::hash! {
187        "usr": user_id,
188        "name": opt_in,
189        "value": "true",
190    };
191
192    Ok(!editor.search("aus", query)?.is_empty())
193}
194
195/// Create events for a passive-hook event definition, returning the
196/// IDs of the created events on success.
197///
198/// Caller is responsible for beginning / committing the transaction.
199pub fn create_passive_events_for_def(
200    editor: &mut Editor,
201    event_def_id: i64,
202    location_field: &str,
203    mut filter_op: Option<EgValue>,
204) -> EgResult<Option<Vec<i64>>> {
205    let flesh = eg::hash! {
206        "flesh": 1,
207        "flesh_fields": {
208            "atevdef": ["hook"]
209        }
210    };
211
212    let event_def = editor
213        .retrieve_with_ops("atevdef", event_def_id, flesh)?
214        .ok_or_else(|| editor.die_event())?;
215
216    let mut filters = match filter_op.take() {
217        Some(f) => f,
218        None => eg::hash! {},
219    };
220
221    // Limit to targets within range of our event def.
222    filters[location_field] = eg::hash! {
223        "in": {
224            "select": {
225                "aou": [{
226                    "column": "id",
227                    "transform": "actor.org_unit_descendants",
228                    "result_field": "id"
229                }],
230            },
231            "from": "aou",
232            "where": {"id": event_def["owner"].clone()}
233        }
234    };
235
236    // Determine the date range of the items we want to target.
237
238    let def_delay = event_def["delay"].as_str().unwrap(); // required
239    let delay_dt = date::add_interval(date::now(), def_delay)?;
240
241    let delay_filter;
242    if let Some(max_delay) = event_def["max_delay"].as_str() {
243        let max_delay_dt = date::add_interval(date::now(), max_delay)?;
244
245        if max_delay_dt < delay_dt {
246            delay_filter = eg::hash! {
247                "between": [
248                    date::to_iso(&max_delay_dt),
249                    date::to_iso(&delay_dt),
250                ]
251            };
252        } else {
253            delay_filter = eg::hash! {
254                "between": [
255                    date::to_iso(&delay_dt),
256                    date::to_iso(&max_delay_dt),
257                ]
258            };
259        }
260    } else {
261        delay_filter = eg::hash! {"<=": date::to_iso(&delay_dt)};
262    }
263
264    let delay_field = event_def["delay_field"]
265        .as_str()
266        .ok_or_else(|| "Passive event defs require a delay_field".to_string())?;
267
268    filters[delay_field] = delay_filter;
269
270    // Make sure we don't create events that are already represented.
271
272    let core_type = event_def["hook"]["core_type"].as_str().unwrap(); // required
273                                                                      //let idl_class = idl::get_class(core_type)?.clone();
274    let idl_class = idl::get_class(core_type)?;
275
276    let pkey_field = idl_class
277        .pkey()
278        .ok_or_else(|| format!("IDL class {core_type} has no primary key"))?;
279
280    let mut join = eg::hash! {
281        "join": {
282            "atev": {
283                "field": "target",
284                "fkey": pkey_field,
285                "type": "left",
286                "filter": {"event_def": event_def_id}
287            }
288        }
289    };
290
291    // Some event types are repeatable depending on a repeat delay.
292    if let Some(rpt_delay) = event_def["repeat_delay"].as_str() {
293        let delay_dt = date::add_interval(date::now(), rpt_delay)?;
294
295        join["join"]["atev"]["filter"] = eg::hash! {
296            "start_time": {">": date::to_iso(&delay_dt)}
297        }
298    }
299
300    // Skip targets where the user is not opted in.
301    if let Some(usr_field) = event_def["usr_field"].as_str() {
302        if let Some(setting) = event_def["opt_in_setting"].as_str() {
303            // {"+circ": "usr"}
304            let mut user_matches = eg::hash! {};
305            user_matches[&format!("+{core_type}")] = EgValue::from(usr_field);
306
307            let opt_filter = eg::hash! {
308                "-exists": {
309                    "from": "aus",
310                    "where": {
311                        "name": setting,
312                        "usr": {"=": user_matches},
313                        "value": "true"
314                    }
315                }
316            };
317
318            if filters["-and"].is_array() {
319                filters["-and"].push(opt_filter).expect("Is Array");
320            } else {
321                filters["-and"] = eg::array![opt_filter];
322            }
323        }
324    }
325
326    log::debug!("Event def {event_def_id} filter is: {}", filters.dump());
327
328    editor.set_timeout(3600); // 1hr
329
330    let targets = editor.search(core_type, filters)?;
331
332    editor.reset_timeout();
333
334    if targets.is_empty() {
335        log::info!("No targets found for event def {event_def_id}");
336        return Ok(None);
337    } else {
338        log::info!(
339            "Found {} targets for vent def {event_def_id}",
340            targets.len()
341        );
342    }
343
344    let mut result_ids = Vec::new();
345
346    for target in targets {
347        let id = target[pkey_field].to_string();
348
349        let mut event = eg::hash! {
350            "target": id,
351            "event_def": event_def_id,
352            "run_time": "now",
353        };
354
355        event.bless("atev")?;
356
357        let event = editor.create(event)?;
358
359        result_ids.push(event.id()?);
360    }
361
362    log::info!("Done creating events for event_def {event_def_id}");
363
364    Ok(Some(result_ids))
365}