evergreen/common/trigger/
mod.rs1use 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
17pub 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 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
67pub 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 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#[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
136fn calc_runtime(event_def: &EgValue, target: &EgValue) -> EgResult<Option<String>> {
140 if !event_def["passive"].boolish() {
141 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), };
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
166fn 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 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
195pub 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 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 let def_delay = event_def["delay"].as_str().unwrap(); 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 let core_type = event_def["hook"]["core_type"].as_str().unwrap(); 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 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 if let Some(usr_field) = event_def["usr_field"].as_str() {
302 if let Some(setting) = event_def["opt_in_setting"].as_str() {
303 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); 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}