LP1819179 PCRUD selector fleshing handles maps
[Evergreen.git] / Open-ILS / src / eg2 / src / app / core / pcrud.service.ts
1 import {Injectable} from '@angular/core';
2 import {Observable, Observer} from 'rxjs';
3 import {IdlService, IdlObject} from './idl.service';
4 import {NetService, NetRequest} from './net.service';
5 import {AuthService} from './auth.service';
6
7 // Externally defined.  Used here for debugging.
8 declare var js2JSON: (jsThing: any) => string;
9 declare var OpenSRF: any; // creating sessions
10
11 interface PcrudReqOps {
12     authoritative?: boolean;
13     anonymous?: boolean;
14     idlist?: boolean;
15     atomic?: boolean;
16     // If true, link-type fields which link to a class that defines a
17     // selector will be fleshed with the linked value.  This affects
18     // retrieve(), retrieveAll(), and search() calls.
19     fleshSelectors?: boolean;
20 }
21
22 // For for documentation purposes.
23 type PcrudResponse = any;
24
25 export class PcrudContext {
26
27     static verboseLogging = true; //
28     static identGenerator = 0; // for debug logging
29
30     private ident: number;
31     private authoritative: boolean;
32     private xactCloseMode: string;
33     private cudIdx: number;
34     private cudAction: string;
35     private cudLast: PcrudResponse;
36     private cudList: IdlObject[];
37
38     private idl: IdlService;
39     private net: NetService;
40     private auth: AuthService;
41
42     // Tracks nested CUD actions
43     cudObserver: Observer<PcrudResponse>;
44
45     session: any; // OpenSRF.ClientSession
46
47     constructor( // passed in by parent service -- not injected
48         egIdl: IdlService,
49         egNet: NetService,
50         egAuth: AuthService
51     ) {
52         this.idl = egIdl;
53         this.net = egNet;
54         this.auth = egAuth;
55         this.xactCloseMode = 'rollback';
56         this.ident = PcrudContext.identGenerator++;
57         this.session = new OpenSRF.ClientSession('open-ils.pcrud');
58     }
59
60     toString(): string {
61         return '[PCRUDContext ' + this.ident + ']';
62     }
63
64     log(msg: string): void {
65         if (PcrudContext.verboseLogging) {
66             console.debug(this + ': ' + msg);
67         }
68     }
69
70     err(msg: string): void {
71         console.error(this + ': ' + msg);
72     }
73
74     token(reqOps?: PcrudReqOps): string {
75         return (reqOps && reqOps.anonymous) ?
76             'ANONYMOUS' : this.auth.token();
77     }
78
79     connect(): Promise<PcrudContext> {
80         this.log('connect');
81         return new Promise( (resolve, reject) => {
82             this.session.connect({
83                 onconnect : () => { resolve(this); }
84             });
85         });
86     }
87
88     disconnect(): void {
89         this.log('disconnect');
90         this.session.disconnect();
91     }
92
93     // Adds "flesh" logic to retrieve linked values for all fields
94     // that link to a class which defines a selector field.
95     applySelectorFleshing(fmClass: string, pcrudOps: any) {
96         pcrudOps = pcrudOps || {};
97
98         if (!pcrudOps.flesh) {
99             pcrudOps.flesh = 1;
100         }
101
102         if (!pcrudOps.flesh_fields) {
103             pcrudOps.flesh_fields = {};
104         }
105
106         this.idl.classes[fmClass].fields
107         .filter(f =>
108             f.datatype === 'link' && (
109                 f.reltype === 'has_a' || f.reltype === 'might_have'
110             )
111         ).forEach(field => {
112
113             const selector = this.idl.getLinkSelector(fmClass, field.name);
114             if (!selector) { return; }
115
116             if (field.map) {
117                 // For mapped fields, we only want to auto-flesh them
118                 // if both steps along the path are single-row fleshers.
119
120                 const mapClass = field['class'];
121                 const mapField = field.map;
122                 const def = this.idl.classes[mapClass].field_map[mapField];
123
124                 if (!(def.reltype === 'has_a' ||
125                       def.reltype === 'might_have')) {
126                     // Field maps to a remote field which may contain
127                     // multiple rows.  Skip it.
128                     return;
129                 }
130             }
131
132             if (!pcrudOps.flesh_fields[fmClass]) {
133                 pcrudOps.flesh_fields[fmClass] = [];
134             }
135
136             if (pcrudOps.flesh_fields[fmClass].indexOf(field.name) < 0) {
137                 pcrudOps.flesh_fields[fmClass].push(field.name);
138             }
139         });
140     }
141
142     retrieve(fmClass: string, pkey: Number | string,
143             pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
144         reqOps = reqOps || {};
145         this.authoritative = reqOps.authoritative || false;
146         if (reqOps.fleshSelectors) {
147             this.applySelectorFleshing(fmClass, pcrudOps);
148         }
149         return this.dispatch(
150             `open-ils.pcrud.retrieve.${fmClass}`,
151              [this.token(reqOps), pkey, pcrudOps]);
152     }
153
154     retrieveAll(fmClass: string, pcrudOps?: any,
155             reqOps?: PcrudReqOps): Observable<PcrudResponse> {
156         const search = {};
157         search[this.idl.classes[fmClass].pkey] = {'!=' : null};
158         return this.search(fmClass, search, pcrudOps, reqOps);
159     }
160
161     search(fmClass: string, search: any,
162             pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
163         reqOps = reqOps || {};
164         this.authoritative = reqOps.authoritative || false;
165
166         const returnType = reqOps.idlist ? 'id_list' : 'search';
167         let method = `open-ils.pcrud.${returnType}.${fmClass}`;
168
169         if (reqOps.atomic) { method += '.atomic'; }
170
171         if (reqOps.fleshSelectors) {
172             this.applySelectorFleshing(fmClass, pcrudOps);
173         }
174
175         return this.dispatch(method, [this.token(reqOps), search, pcrudOps]);
176     }
177
178     create(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
179         return this.cud('create', list);
180     }
181     update(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
182         return this.cud('update', list);
183     }
184     remove(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
185         return this.cud('delete', list);
186     }
187     autoApply(list: IdlObject | IdlObject[]): Observable<PcrudResponse> { // RENAMED
188         return this.cud('auto',   list);
189     }
190
191     xactClose(): Observable<PcrudResponse> {
192         return this.sendRequest(
193             'open-ils.pcrud.transaction.' + this.xactCloseMode,
194             [this.token()]
195         );
196     }
197
198     xactBegin(): Observable<PcrudResponse> {
199         return this.sendRequest(
200             'open-ils.pcrud.transaction.begin', [this.token()]
201         );
202     }
203
204     private dispatch(method: string, params: any[]): Observable<PcrudResponse> {
205         if (this.authoritative) {
206             return this.wrapXact(() => {
207                 return this.sendRequest(method, params);
208             });
209         } else {
210             return this.sendRequest(method, params);
211         }
212     }
213
214
215     // => connect
216     // => xact_begin
217     // => action
218     // => xact_close(commit/rollback)
219     // => disconnect
220     wrapXact(mainFunc: () => Observable<PcrudResponse>): Observable<PcrudResponse> {
221         return Observable.create(observer => {
222
223             // 1. connect
224             this.connect()
225
226             // 2. start the transaction
227             .then(() => this.xactBegin().toPromise())
228
229             // 3. execute the main body
230             .then(() => {
231
232                 mainFunc().subscribe(
233                     res => observer.next(res),
234                     err => observer.error(err),
235                     ()  => {
236                         this.xactClose().toPromise().then(() => {
237                             // 5. disconnect
238                             this.disconnect();
239                             // 6. all done
240                             observer.complete();
241                         });
242                     }
243                 );
244             });
245         });
246     }
247
248     private sendRequest(method: string,
249             params: any[]): Observable<PcrudResponse> {
250
251         // this.log(`sendRequest(${method})`);
252
253         return this.net.requestCompiled(
254             new NetRequest(
255                 'open-ils.pcrud', method, params, this.session)
256         );
257     }
258
259     private cud(action: string,
260         list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
261         this.cudList = [].concat(list); // value or array
262
263         this.log(`CUD(): ${action}`);
264
265         this.cudIdx = 0;
266         this.cudAction = action;
267         this.xactCloseMode = 'commit';
268
269         return this.wrapXact(() => {
270             return Observable.create(observer => {
271                 this.cudObserver = observer;
272                 this.nextCudRequest();
273             });
274         });
275     }
276
277     /**
278      * Loops through the list of objects to update and sends
279      * them one at a time to the server for processing.  Once
280      * all are done, the cudObserver is resolved.
281      */
282     nextCudRequest(): void {
283         if (this.cudIdx >= this.cudList.length) {
284             this.cudObserver.complete();
285             return;
286         }
287
288         let action = this.cudAction;
289         const fmObj = this.cudList[this.cudIdx++];
290
291         if (action === 'auto') {
292             if (fmObj.ischanged()) { action = 'update'; }
293             if (fmObj.isnew())     { action = 'create'; }
294             if (fmObj.isdeleted()) { action = 'delete'; }
295
296             if (action === 'auto') {
297                 // object does not need updating; move along
298                 this.nextCudRequest();
299             }
300         }
301
302         this.sendRequest(
303             `open-ils.pcrud.${action}.${fmObj.classname}`,
304             [this.token(), fmObj]
305         ).subscribe(
306             res => this.cudObserver.next(res),
307             err => this.cudObserver.error(err),
308             ()  => this.nextCudRequest()
309         );
310     }
311 }
312
313 @Injectable({providedIn: 'root'})
314 export class PcrudService {
315
316     constructor(
317         private idl: IdlService,
318         private net: NetService,
319         private auth: AuthService
320     ) {}
321
322     // Pass-thru functions for one-off PCRUD calls
323
324     connect(): Promise<PcrudContext> {
325         return this.newContext().connect();
326     }
327
328     newContext(): PcrudContext {
329         return new PcrudContext(this.idl, this.net, this.auth);
330     }
331
332     retrieve(fmClass: string, pkey: Number | string,
333         pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
334         return this.newContext().retrieve(fmClass, pkey, pcrudOps, reqOps);
335     }
336
337     retrieveAll(fmClass: string, pcrudOps?: any,
338         reqOps?: PcrudReqOps): Observable<PcrudResponse> {
339         return this.newContext().retrieveAll(fmClass, pcrudOps, reqOps);
340     }
341
342     search(fmClass: string, search: any,
343         pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
344         return this.newContext().search(fmClass, search, pcrudOps, reqOps);
345     }
346
347     create(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
348         return this.newContext().create(list);
349     }
350
351     update(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
352         return this.newContext().update(list);
353     }
354
355     remove(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
356         return this.newContext().remove(list);
357     }
358
359     autoApply(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
360         return this.newContext().autoApply(list);
361     }
362 }
363
364