]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts
LP#1801984 Upgrading Angular 6 to Angular 7
[Evergreen.git] / Open-ILS / src / eg2 / src / app / share / catalog / bib-record.service.ts
1 import {Injectable} from '@angular/core';
2 import {Observable, from} from 'rxjs';
3 import {mergeMap, map} from 'rxjs/operators';
4 import {OrgService} from '@eg/core/org.service';
5 import {UnapiService} from '@eg/share/catalog/unapi.service';
6 import {IdlService, IdlObject} from '@eg/core/idl.service';
7 import {NetService} from '@eg/core/net.service';
8 import {PcrudService} from '@eg/core/pcrud.service';
9
10 export const NAMESPACE_MAPS = {
11     'mods':     'http://www.loc.gov/mods/v3',
12     'biblio':   'http://open-ils.org/spec/biblio/v1',
13     'holdings': 'http://open-ils.org/spec/holdings/v1',
14     'indexing': 'http://open-ils.org/spec/indexing/v1'
15 };
16
17 export const HOLDINGS_XPATH =
18     '/holdings:holdings/holdings:counts/holdings:count';
19
20
21 export class BibRecordSummary {
22     id: number; // == record.id() for convenience
23     orgId: number;
24     orgDepth: number;
25     record: IdlObject;
26     display: any;
27     attributes: any;
28     holdingsSummary: any;
29     holdCount: number;
30     bibCallNumber: string;
31     net: NetService;
32
33     constructor(record: IdlObject, orgId: number, orgDepth: number) {
34         this.id = record.id();
35         this.record = record;
36         this.orgId = orgId;
37         this.orgDepth = orgDepth;
38         this.display = {};
39         this.attributes = {};
40         this.bibCallNumber = null;
41     }
42
43     ingest() {
44         this.compileDisplayFields();
45         this.compileRecordAttrs();
46
47         // Normalize some data for JS consistency
48         this.record.creator(Number(this.record.creator()));
49         this.record.editor(Number(this.record.editor()));
50     }
51
52     compileDisplayFields() {
53         this.record.flat_display_entries().forEach(entry => {
54             if (entry.multi() === 't') {
55                 if (this.display[entry.name()]) {
56                     this.display[entry.name()].push(entry.value());
57                 } else {
58                     this.display[entry.name()] = [entry.value()];
59                 }
60             } else {
61                 this.display[entry.name()] = entry.value();
62             }
63         });
64     }
65
66     compileRecordAttrs() {
67         // Any attr can be multi-valued.
68         this.record.mattrs().forEach(attr => {
69             if (this.attributes[attr.attr()]) {
70                 this.attributes[attr.attr()].push(attr.value());
71             } else {
72                 this.attributes[attr.attr()] = [attr.value()];
73             }
74         });
75     }
76
77     // Get -> Set -> Return bib hold count
78     getHoldCount(): Promise<number> {
79
80         if (Number.isInteger(this.holdCount)) {
81             return Promise.resolve(this.holdCount);
82         }
83
84         return this.net.request(
85             'open-ils.circ',
86             'open-ils.circ.bre.holds.count', this.id
87         ).toPromise().then(count => this.holdCount = count);
88     }
89
90     // Get -> Set -> Return bib-level call number
91     getBibCallNumber(): Promise<string> {
92
93         if (this.bibCallNumber !== null) {
94             return Promise.resolve(this.bibCallNumber);
95         }
96
97         // TODO labelClass = cat.default_classification_scheme YAOUS
98         const labelClass = 1;
99
100         return this.net.request(
101             'open-ils.cat',
102             'open-ils.cat.biblio.record.marc_cn.retrieve',
103             this.id, labelClass
104         ).toPromise().then(cnArray => {
105             if (cnArray && cnArray.length > 0) {
106                 const key1 = Object.keys(cnArray[0])[0];
107                 this.bibCallNumber = cnArray[0][key1];
108             } else {
109                 this.bibCallNumber = '';
110             }
111             return this.bibCallNumber;
112         });
113     }
114 }
115
116 @Injectable()
117 export class BibRecordService {
118
119     // Cache of bib editor / creator objects
120     // Assumption is this list will be limited in size.
121     userCache: {[id: number]: IdlObject};
122
123     constructor(
124         private idl: IdlService,
125         private net: NetService,
126         private org: OrgService,
127         private unapi: UnapiService,
128         private pcrud: PcrudService
129     ) {
130         this.userCache = {};
131     }
132
133     // Avoid fetching the MARC blob by specifying which fields on the
134     // bre to select.  Note that fleshed fields are explicitly selected.
135     fetchableBreFields(): string[] {
136         return this.idl.classes.bre.fields
137             .filter(f => !f.virtual && f.name !== 'marc')
138             .map(f => f.name);
139     }
140
141     // Note when multiple IDs are provided, responses are emitted in order
142     // of receipt, not necessarily in the requested ID order.
143     getBibSummary(bibIds: number | number[],
144         orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
145
146         const ids = [].concat(bibIds);
147
148         if (ids.length === 0) {
149             return from([]);
150         }
151
152         return this.pcrud.search('bre', {id: ids},
153             {   flesh: 1,
154                 flesh_fields: {bre: ['flat_display_entries', 'mattrs']},
155                 select: {bre : this.fetchableBreFields()}
156             },
157             {anonymous: true} // skip unneccesary auth
158         ).pipe(mergeMap(bib => {
159             const summary = new BibRecordSummary(bib, orgId, orgDepth);
160             summary.net = this.net; // inject
161             summary.ingest();
162             return this.getHoldingsSummary(bib.id(), orgId, orgDepth)
163             .then(holdingsSummary => {
164                 summary.holdingsSummary = holdingsSummary;
165                 return summary;
166             });
167         }));
168     }
169
170     // Flesh the creator and editor fields.
171     // Handling this separately lets us pull from the cache and
172     // avoids the requirement that the main bib query use a staff
173     // (VIEW_USER) auth token.
174     fleshBibUsers(records: IdlObject[]): Promise<void> {
175
176         const search = [];
177
178         records.forEach(rec => {
179             ['creator', 'editor'].forEach(field => {
180                 const id = rec[field]();
181                 if (Number.isInteger(id)) {
182                     if (this.userCache[id]) {
183                         rec[field](this.userCache[id]);
184                     } else if (!search.includes(id)) {
185                         search.push(id);
186                     }
187                 }
188             });
189         });
190
191         if (search.length === 0) {
192             return Promise.resolve();
193         }
194
195         return this.pcrud.search('au', {id: search})
196         .pipe(map(user => {
197             this.userCache[user.id()] = user;
198             records.forEach(rec => {
199                 if (user.id() === rec.creator()) {
200                     rec.creator(user);
201                 }
202                 if (user.id() === rec.editor()) {
203                     rec.editor(user);
204                 }
205             });
206         })).toPromise();
207     }
208
209     getHoldingsSummary(recordId: number,
210         orgId: number, orgDepth: number): Promise<any> {
211
212         const holdingsSummary = [];
213
214         return this.unapi.getAsXmlDocument({
215             target: 'bre',
216             id: recordId,
217             extras: '{holdings_xml}',
218             format: 'holdings_xml',
219             orgId: orgId,
220             depth: orgDepth
221         }).then(xmlDoc => {
222
223             // namespace resolver
224             const resolver: any = (prefix: string): string => {
225                 return NAMESPACE_MAPS[prefix] || null;
226             };
227
228             // Extract the holdings data from the unapi xml doc
229             const result = xmlDoc.evaluate(HOLDINGS_XPATH,
230                 xmlDoc, resolver, XPathResult.ANY_TYPE, null);
231
232             let node;
233             while (node = result.iterateNext()) {
234                 const counts = {type : node.getAttribute('type')};
235                 ['depth', 'org_unit', 'transcendant',
236                     'available', 'count', 'unshadow'].forEach(field => {
237                     counts[field] = Number(node.getAttribute(field));
238                 });
239                 holdingsSummary.push(counts);
240             }
241
242             return holdingsSummary;
243         });
244     }
245 }
246
247