evergreen/
editor.rs

1//! Create, Retrieve, Update, Delete IDL-classed objects via (by default) open-ils.cstore.
2use crate as eg;
3use eg::event::EgEvent;
4use eg::idl;
5use eg::osrf::params::ApiParams;
6use eg::result::{EgError, EgResult};
7use eg::Client;
8use eg::ClientSession;
9use eg::EgValue;
10
11const DEFAULT_TIMEOUT: u64 = 60;
12
13/// Specifies Which service are we communicating with.
14#[derive(Debug, Clone, PartialEq)]
15pub enum Personality {
16    Cstore,
17    Pcrud,
18    ReporterStore,
19}
20
21impl From<&str> for Personality {
22    fn from(s: &str) -> Self {
23        match s {
24            "open-ils.pcrud" => Self::Pcrud,
25            "open-ils.reporter-store" => Self::ReporterStore,
26            _ => Self::Cstore,
27        }
28    }
29}
30
31impl From<&Personality> for &str {
32    fn from(p: &Personality) -> &'static str {
33        match *p {
34            Personality::Cstore => "open-ils.cstore",
35            Personality::Pcrud => "open-ils.pcrud",
36            Personality::ReporterStore => "open-ils.reporter-store",
37        }
38    }
39}
40
41/*
42pub struct QueryOps {
43    limit: Option<usize>,
44    offset: Option<usize>,
45    order_by: Option<(String, String)>,
46}
47*/
48
49pub struct Editor {
50    client: Client,
51    session: Option<ClientSession>,
52
53    personality: Personality,
54    authtoken: Option<String>,
55    authtime: Option<usize>,
56    requestor: Option<EgValue>,
57    timeout: u64,
58
59    /// True if the caller wants us to perform actions within
60    /// a transaction.  Write actions require this.
61    xact_wanted: bool,
62
63    /// ID for currently active transaction.
64    xact_id: Option<String>,
65
66    /// Most recent non-success event
67    last_event: Option<EgEvent>,
68
69    has_pending_changes: bool,
70}
71
72impl Clone for Editor {
73    fn clone(&self) -> Editor {
74        let mut e = Editor::new(&self.client);
75        e.personality = self.personality().clone();
76        e.authtoken = self.authtoken().map(str::to_string);
77        e.requestor = self.requestor().cloned();
78        e
79    }
80}
81
82impl Editor {
83    /// Create a new minimal Editor
84    pub fn new(client: &Client) -> Self {
85        Editor {
86            client: client.clone(),
87            personality: "".into(),
88            timeout: DEFAULT_TIMEOUT,
89            xact_wanted: false,
90            xact_id: None,
91            session: None,
92            authtoken: None,
93            authtime: None,
94            requestor: None,
95            last_event: None,
96            has_pending_changes: false,
97        }
98    }
99
100    /// Apply a new request timeout value in seconds.
101    pub fn set_timeout(&mut self, timeout: u64) {
102        self.timeout = timeout;
103    }
104
105    /// Reset to the default timeout
106    pub fn reset_timeout(&mut self) {
107        self.timeout = DEFAULT_TIMEOUT;
108    }
109
110    pub fn client_mut(&mut self) -> &mut Client {
111        &mut self.client
112    }
113
114    /// True if create/update/delete have been called within a
115    /// transaction that has yet to be committed or rolled back.
116    ///
117    /// This has no effect on the Editor, but may be useful to
118    /// the calling code.
119    pub fn has_pending_changes(&self) -> bool {
120        self.has_pending_changes
121    }
122
123    /// Create an editor with an existing authtoken
124    pub fn with_auth(client: &Client, authtoken: &str) -> Self {
125        let mut editor = Editor::new(client);
126        editor.authtoken = Some(authtoken.to_string());
127        editor
128    }
129
130    /// Create an editor with an existing authtoken, with the "transaction
131    /// wanted" flag set by default.
132    pub fn with_auth_xact(client: &Client, authtoken: &str) -> Self {
133        let mut editor = Editor::new(client);
134        editor.authtoken = Some(authtoken.to_string());
135        editor.xact_wanted = true;
136        editor
137    }
138
139    /// Verify our authtoken is still valid.
140    ///
141    /// Update our "requestor" object to match the user object linked
142    /// to the authtoken in the cache.
143    pub fn checkauth(&mut self) -> EgResult<bool> {
144        let token = match self.authtoken() {
145            Some(t) => t,
146            None => return Ok(false),
147        };
148
149        let service = "open-ils.auth";
150        let method = "open-ils.auth.session.retrieve";
151        let params = vec![EgValue::from(token), EgValue::from(true)];
152
153        let mut ses = self.client.session(service);
154        let resp_op = ses.request(method, params)?.first()?;
155
156        if let Some(user) = resp_op {
157            if let Some(evt) = EgEvent::parse(&user) {
158                log::debug!("Editor checkauth call returned non-success event: {}", evt);
159                self.set_last_event(evt);
160                return Ok(false);
161            }
162
163            if user.has_key("usrname") {
164                self.requestor = Some(user);
165                return Ok(true);
166            }
167        }
168
169        log::debug!("Editor checkauth call returned unexpected data");
170
171        // Login failure is not considered an error.
172        self.set_last_event(EgEvent::new("NO_SESSION"));
173        Ok(false)
174    }
175
176    /// Delete the auth session and remove any trace of the login session
177    /// from within.
178    pub fn clear_auth(&mut self) -> EgResult<()> {
179        self.requestor = None;
180
181        let token = match self.authtoken.take() {
182            Some(t) => t,
183            None => return Ok(()),
184        };
185
186        let service = "open-ils.auth";
187        let method = "open-ils.auth.session.retrieve";
188
189        let mut ses = self.client.session(service);
190        ses.request(method, token).map(|_| ())
191    }
192
193    pub fn personality(&self) -> &Personality {
194        &self.personality
195    }
196
197    pub fn authtoken(&self) -> Option<&str> {
198        self.authtoken.as_deref()
199    }
200
201    /// Set the authtoken value.
202    pub fn set_authtoken(&mut self, token: &str) {
203        self.authtoken = Some(token.to_string())
204    }
205
206    /// Set the authtoken value and verify the authtoken is valid
207    pub fn apply_authtoken(&mut self, token: &str) -> EgResult<bool> {
208        self.set_authtoken(token);
209        self.checkauth()
210    }
211
212    pub fn authtime(&self) -> Option<usize> {
213        self.authtime
214    }
215
216    fn has_xact_id(&self) -> bool {
217        self.xact_id.is_some()
218    }
219
220    /// ID of the requestor.
221    pub fn requestor_id(&self) -> EgResult<i64> {
222        if let Some(req) = self.requestor() {
223            req.id()
224        } else {
225            Err("Editor has no requestor".into())
226        }
227    }
228
229    /// Org Unit ID of the requestor's workstation.
230    ///
231    /// Panics if requestor value is unset.
232    pub fn requestor_ws_ou(&self) -> Option<i64> {
233        if let Some(req) = self.requestor() {
234            req["ws_ou"].as_int()
235        } else {
236            None
237        }
238    }
239
240    /// Workstation ID of the requestor's workstation.
241    ///
242    /// Panics if requestor value is unset.
243    pub fn requestor_ws_id(&self) -> Option<i64> {
244        if let Some(r) = self.requestor() {
245            r["wsid"].as_int()
246        } else {
247            None
248        }
249    }
250
251    /// Workstation ID of the requestor's workstation.
252    ///
253    /// Panics if requestor value is unset.
254    pub fn requestor_home_ou(&self) -> EgResult<i64> {
255        if let Some(r) = self.requestor() {
256            r["home_ou"].int()
257        } else {
258            Err("Editor has no requestor".into())
259        }
260    }
261
262    pub fn perm_org(&self) -> i64 {
263        self.requestor_ws_ou()
264            .unwrap_or(self.requestor_home_ou().unwrap_or(-1))
265    }
266
267    pub fn requestor(&self) -> Option<&EgValue> {
268        self.requestor.as_ref()
269    }
270
271    /// True if a requestor is set
272    pub fn has_requestor(&self) -> bool {
273        self.requestor.is_some()
274    }
275
276    pub fn set_requestor(&mut self, r: &EgValue) {
277        self.requestor = Some(r.clone())
278    }
279
280    /// Same as set_requestor, but takes ownership of the value.
281    pub fn give_requestor(&mut self, r: EgValue) {
282        self.requestor = Some(r);
283    }
284
285    pub fn last_event(&self) -> Option<&EgEvent> {
286        self.last_event.as_ref()
287    }
288
289    pub fn take_last_event(&mut self) -> Option<EgEvent> {
290        self.last_event.take()
291    }
292
293    pub fn event_as_err(&self) -> EgError {
294        match self.last_event() {
295            Some(e) => EgError::from_event(e.clone()),
296            None => EgError::from_string("Editor Has No Event".to_string()),
297        }
298    }
299
300    /// Returns our last event as JSON or JsonValue::Null if we have
301    /// no last event.
302    pub fn event(&self) -> EgValue {
303        match self.last_event() {
304            Some(e) => e.to_value(),
305            None => EgValue::Null,
306        }
307    }
308
309    fn set_last_event(&mut self, evt: EgEvent) {
310        self.last_event = Some(evt);
311    }
312
313    /// Rollback the active transaction, disconnect from the worker,
314    /// and return an EgError-wrapped variant of the last event.
315    ///
316    /// The raw event can still be accessed via self.last_event().
317    pub fn die_event(&mut self) -> EgError {
318        if let Err(e) = self.rollback() {
319            return e;
320        }
321        match self.last_event() {
322            Some(e) => EgError::from_event(e.clone()),
323            None => EgError::from_string("Die-Event Called With No Event".to_string()),
324        }
325    }
326
327    /// Rollback the active transaction, disconnect from the worker,
328    /// and an EgError using the provided message as either the
329    /// debug text on our last_event or as the string contents
330    /// of an EgError::Debug variant.
331    pub fn die_event_msg(&mut self, msg: &str) -> EgError {
332        if let Err(e) = self.rollback() {
333            return e;
334        }
335        match self.last_event() {
336            Some(e) => {
337                let mut e2 = e.clone();
338                e2.set_debug(msg);
339                EgError::from_event(e2)
340            }
341            None => EgError::from_string(msg.to_string()),
342        }
343    }
344
345    /// Rollback the active transaction and disconnect from the worker.
346    pub fn rollback(&mut self) -> EgResult<()> {
347        self.xact_rollback()?;
348        self.disconnect()
349    }
350
351    /// Commit the active transaction and disconnect from the worker.
352    pub fn commit(&mut self) -> EgResult<()> {
353        self.xact_commit()?;
354        self.disconnect()
355    }
356
357    /// Generate a method name prefixed with the app name of our personality.
358    fn app_method(&self, part: &str) -> String {
359        let p: &str = self.personality().into();
360        format!("{p}.{}", part)
361    }
362
363    pub fn in_transaction(&self) -> bool {
364        if let Some(ref ses) = self.session {
365            ses.connected() && self.has_xact_id()
366        } else {
367            false
368        }
369    }
370
371    /// Rollback a database transaction.
372    ///
373    /// This variation does not send a DISCONNECT to the connected worker.
374    pub fn xact_rollback(&mut self) -> EgResult<()> {
375        if self.in_transaction() {
376            self.request_np(&self.app_method("transaction.rollback"))?;
377        }
378
379        self.xact_id = None;
380        self.xact_wanted = false;
381        self.has_pending_changes = false;
382
383        Ok(())
384    }
385
386    /// Start a new transaction, connecting to a worker if necessary.
387    pub fn xact_begin(&mut self) -> EgResult<()> {
388        self.connect()?;
389        if let Some(id) = self.request_np(&self.app_method("transaction.begin"))? {
390            if let Some(id_str) = id.as_str() {
391                log::debug!("New transaction started with id {}", id_str);
392                self.xact_id = Some(id_str.to_string());
393            }
394        }
395        Ok(())
396    }
397
398    /// Commit a database transaction.
399    ///
400    /// This variation does not send a DISCONNECT to the connected worker.
401    pub fn xact_commit(&mut self) -> EgResult<()> {
402        if self.in_transaction() {
403            // We can take() the xact_id here because we're clearing
404            // it below anyway.  This avoids a .to_string() as a way
405            // to get around the mutable borrow from self.request().
406            let xact_id = self.xact_id.take().unwrap();
407            let method = self.app_method("transaction.commit");
408            self.request(&method, xact_id)?;
409        }
410
411        self.xact_id = None;
412        self.xact_wanted = false;
413        self.has_pending_changes = false;
414
415        Ok(())
416    }
417
418    /// End the stateful conversation with the remote worker.
419    pub fn disconnect(&mut self) -> EgResult<()> {
420        self.xact_rollback()?;
421
422        if let Some(ref ses) = self.session {
423            ses.disconnect()?;
424        }
425        self.session = None;
426        Ok(())
427    }
428
429    /// Start a stateful conversation with a worker.
430    pub fn connect(&mut self) -> EgResult<()> {
431        if let Some(ref ses) = self.session {
432            if ses.connected() {
433                // Already connected.
434                return Ok(());
435            }
436        }
437        self.session().connect()?;
438        Ok(())
439    }
440
441    /// Send an API request without any parameters.
442    ///
443    /// See request() for more.
444    fn request_np(&mut self, method: &str) -> EgResult<Option<EgValue>> {
445        let params: Vec<EgValue> = Vec::new();
446        self.request(method, params)
447    }
448
449    fn logtag(&self) -> String {
450        let requestor = match self.requestor() {
451            Some(req) => req.id().unwrap_or(0),
452            _ => 0,
453        };
454
455        format!(
456            "editor[{}|{}]",
457            match self.has_xact_id() {
458                true => "1",
459                _ => "0",
460            },
461            requestor
462        )
463    }
464
465    /// Format a set of API parameters for debug logging.
466    fn args_to_string(&self, params: &ApiParams) -> String {
467        let mut buf = String::new();
468        for p in params.params().iter() {
469            if let Some(pkv) = p.pkey_value() {
470                if pkv.is_null() {
471                    buf.push_str("<new object>");
472                } else {
473                    buf.push_str(&pkv.dump());
474                }
475            } else {
476                // Not an IDL object, likely a scalar value.
477                buf.push_str(&p.dump());
478            }
479
480            buf.push(' ');
481        }
482
483        buf.trim().to_string()
484    }
485
486    /// Send an API request to our service/worker with parameters.
487    ///
488    /// All requests return at most a single response.
489    fn request(&mut self, method: &str, params: impl Into<ApiParams>) -> EgResult<Option<EgValue>> {
490        let params: ApiParams = params.into();
491
492        log::info!(
493            "{} request {} {}",
494            self.logtag(),
495            method,
496            self.args_to_string(&params)
497        );
498
499        if method.contains("create") || method.contains("update") || method.contains("delete") {
500            if !self.has_xact_id() {
501                self.disconnect()?;
502                Err(format!(
503                    "Attempt to update DB while not in a transaction : {method}"
504                ))?;
505            }
506
507            if params.params().is_empty() {
508                return Err("Create/update/delete calls require a parameter".into());
509            }
510
511            // Write calls also get logged to the activity log
512            log::info!(
513                "ACT:{} request {} {}",
514                self.logtag(),
515                method,
516                self.args_to_string(&params)
517            );
518        }
519
520        let mut req = self.session().request(method, params).or_else(|e| {
521            self.rollback()?;
522            Err(e)
523        })?;
524
525        req.first_with_timeout(self.timeout)
526    }
527
528    /// Returns our mutable session, creating a new one if needed.
529    ///
530    /// New sessions are created for all outbound requests unless
531    /// we're in a connected session.
532    ///
533    /// NOTE: Maintaining the same session thread for for connected
534    /// communication is only strictly necessary when connecting to
535    /// start a cstore, etc. transaction.
536    fn session(&mut self) -> &mut ClientSession {
537        if let Some(ref ses) = self.session {
538            if ses.connected() {
539                return self.session.as_mut().unwrap();
540            }
541        }
542        self.session = Some(self.client.session(self.personality().into()));
543        self.session.as_mut().unwrap()
544    }
545
546    /// Returns the fieldmapper value for the IDL class, replacing
547    /// "::" with "." so the value matches how it's formatted in
548    /// cstore, etc. API calls.
549    fn get_fieldmapper(&self, value: &EgValue) -> EgResult<String> {
550        if let Some(cls) = value.idl_class() {
551            if let Some(fm) = cls.fieldmapper() {
552                return Ok(fm.replace("::", "."));
553            }
554        }
555        Err(format!("Cannot determine fieldmapper from {}", value.dump()).into())
556    }
557
558    fn get_fieldmapper_from_classname(&self, classname: &str) -> EgResult<String> {
559        let cls = idl::get_class(classname)?;
560        if let Some(fm) = cls.fieldmapper() {
561            return Ok(fm.replace("::", "."));
562        }
563        Err(format!("Cannot determine fieldmapper from {classname}").into())
564    }
565
566    /// Execute an atomic json_query call.
567    pub fn json_query(&mut self, query: EgValue) -> EgResult<Vec<EgValue>> {
568        self.json_query_with_ops(query, EgValue::Null)
569    }
570
571    /// Execute an atomic json_query call with additional query params.
572    pub fn json_query_with_ops(&mut self, query: EgValue, ops: EgValue) -> EgResult<Vec<EgValue>> {
573        let method = self.app_method("json_query.atomic");
574
575        let mut params: ApiParams = query.into();
576        if !ops.is_null() {
577            params.add(ops);
578        }
579
580        if let Some(EgValue::Array(vec)) = self.request(&method, params)? {
581            return Ok(vec);
582        }
583
584        Err(format!("Unexpected response to method {method}").into())
585    }
586
587    /// Retrieve an IDL object by its primary key value.
588    pub fn retrieve(
589        &mut self,
590        idlclass: &str,
591        id: impl Into<ApiParams>,
592    ) -> EgResult<Option<EgValue>> {
593        self.retrieve_with_ops(idlclass, id, EgValue::Null)
594    }
595
596    /// Retrieve an IDL object by its primary key value with additional
597    /// query parameters.
598    pub fn retrieve_with_ops(
599        &mut self,
600        idlclass: &str,
601        id: impl Into<ApiParams>,
602        ops: EgValue, // flesh, etc.
603    ) -> EgResult<Option<EgValue>> {
604        let fmapper = self.get_fieldmapper_from_classname(idlclass)?;
605
606        let method = self.app_method(&format!("direct.{fmapper}.retrieve"));
607
608        let mut params: ApiParams = id.into();
609        if !ops.is_null() {
610            params.add(ops);
611        }
612
613        let resp_op = self.request(&method, params)?;
614
615        if resp_op.is_none() {
616            // not-found is not necessarily an error.
617            let key = fmapper.replace('.', "_").to_uppercase();
618            self.set_last_event(EgEvent::new(&format!("{key}_NOT_FOUND")));
619        }
620
621        Ok(resp_op)
622    }
623
624    pub fn search(&mut self, idlclass: &str, query: EgValue) -> EgResult<Vec<EgValue>> {
625        self.search_with_ops(idlclass, query, EgValue::Null)
626    }
627
628    pub fn search_with_ops(
629        &mut self,
630        idlclass: &str,
631        query: EgValue,
632        ops: EgValue, // flesh, etc.
633    ) -> EgResult<Vec<EgValue>> {
634        let fmapper = self.get_fieldmapper_from_classname(idlclass)?;
635
636        let method = self.app_method(&format!("direct.{fmapper}.search.atomic"));
637
638        let mut params: ApiParams = query.into();
639        if !ops.is_null() {
640            params.add(ops);
641        }
642
643        if let Some(EgValue::Array(vec)) = self.request(&method, params)? {
644            return Ok(vec);
645        }
646
647        Err(format!("Unexpected response to method {method}").into())
648    }
649
650    /// Update an object.
651    pub fn update(&mut self, object: EgValue) -> EgResult<()> {
652        if !self.has_xact_id() {
653            return Err("Transaction required for UPDATE".into());
654        }
655
656        let fmapper = self.get_fieldmapper(&object)?;
657
658        let method = self.app_method(&format!("direct.{fmapper}.update"));
659
660        // Update calls return the pkey of the object on success,
661        // nothing on error.
662        if self.request(&method, object)?.is_none() {
663            return Err("Update returned no response".into());
664        }
665
666        self.has_pending_changes = true;
667
668        Ok(())
669    }
670
671    /// Returns the newly created object.
672    pub fn create(&mut self, object: EgValue) -> EgResult<EgValue> {
673        if !self.has_xact_id() {
674            return Err("Transaction required for CREATE".into());
675        }
676
677        let fmapper = self.get_fieldmapper(&object)?;
678
679        let method = self.app_method(&format!("direct.{fmapper}.create"));
680
681        if let Some(resp) = self.request(&method, object)? {
682            if let Some(pkey) = resp.pkey_value() {
683                log::info!("Created new {fmapper} object with pkey: {}", pkey.dump());
684            } else {
685                // Don't think we can get here, but mabye.
686                log::debug!("Created new {fmapper} object: {resp:?}");
687            }
688
689            self.has_pending_changes = true;
690
691            Ok(resp)
692        } else {
693            Err("Create returned no response".into())
694        }
695    }
696
697    /// Delete an IDL Object.
698    ///
699    /// Response is the PKEY value as a JsonValue.
700    pub fn delete(&mut self, object: EgValue) -> EgResult<EgValue> {
701        if !self.has_xact_id() {
702            return Err("Transaction required for DELETE".into());
703        }
704
705        let fmapper = self.get_fieldmapper(&object)?;
706
707        let method = self.app_method(&format!("direct.{fmapper}.delete"));
708
709        if let Some(resp) = self.request(&method, object)? {
710            self.has_pending_changes = true;
711            Ok(resp)
712        } else {
713            Err("Create returned no response".into())
714        }
715    }
716
717    /// Returns Result of true if our authenticated requestor has the
718    /// specified permission at their logged in workstation org unit,
719    /// or their home org unit if no workstation is active.
720    pub fn allowed(&mut self, perm: &str) -> EgResult<bool> {
721        self.allowed_maybe_at(perm, None)
722    }
723
724    /// Returns Result of true if our authenticated requestor has the
725    /// specified permission at the specified org unit.
726    pub fn allowed_at(&mut self, perm: &str, org_id: i64) -> EgResult<bool> {
727        self.allowed_maybe_at(perm, Some(org_id))
728    }
729
730    fn allowed_maybe_at(&mut self, perm: &str, org_id_op: Option<i64>) -> EgResult<bool> {
731        let user_id = match self.requestor_id() {
732            Ok(v) => v,
733            Err(_) => return Ok(false),
734        };
735
736        let org_id = match org_id_op {
737            Some(i) => i,
738            None => self.perm_org(),
739        };
740
741        let query = eg::hash! {
742            "select": {
743                "au": [ {
744                    "transform": "permission.usr_has_perm",
745                    "alias": "has_perm",
746                    "column": "id",
747                    "params": eg::array! [perm, org_id]
748                } ]
749            },
750            "from": "au",
751            "where": {"id": user_id},
752        };
753
754        let resp = self.json_query(query)?;
755        let has_perm = resp[0]["has_perm"].boolish();
756
757        if !has_perm {
758            let mut evt = EgEvent::new("PERM_FAILURE");
759            evt.set_ils_perm(perm);
760            if org_id > 0 {
761                evt.set_ils_perm_loc(org_id);
762            }
763            self.set_last_event(evt);
764        }
765
766        Ok(has_perm)
767    }
768
769    /// Shortcut for eg::osrf::client::Client::send_recv_one()
770    ///
771    /// Slighly simpler than calling editor.client_mut().send_recv_one().
772    pub fn send_recv_one(
773        &mut self,
774        service: &str,
775        method: &str,
776        params: impl Into<ApiParams>,
777    ) -> EgResult<Option<EgValue>> {
778        self.client_mut().send_recv_one(service, method, params)
779    }
780}