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