1 import {Injectable} from '@angular/core';
2 import {Observable, from} from 'rxjs';
3 import {mergeMap, map, tap} 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';
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'
17 export const HOLDINGS_XPATH =
18 '/holdings:holdings/holdings:counts/holdings:count';
21 export class BibRecordSummary {
22 id: number; // == record.id() for convenience
23 metabibId: number; // If present, this is a metabib summary
24 metabibRecords: number[]; // all constituent bib records
32 bibCallNumber: string;
35 constructor(record: IdlObject, orgId: number, orgDepth: number) {
36 this.id = Number(record.id());
39 this.orgDepth = orgDepth;
42 this.bibCallNumber = null;
43 this.metabibRecords = [];
47 this.compileDisplayFields();
48 this.compileRecordAttrs();
50 // Normalize some data for JS consistency
51 this.record.creator(Number(this.record.creator()));
52 this.record.editor(Number(this.record.editor()));
55 compileDisplayFields() {
56 this.record.flat_display_entries().forEach(entry => {
57 if (entry.multi() === 't') {
58 if (this.display[entry.name()]) {
59 this.display[entry.name()].push(entry.value());
61 this.display[entry.name()] = [entry.value()];
64 this.display[entry.name()] = entry.value();
69 compileRecordAttrs() {
70 // Any attr can be multi-valued.
71 this.record.mattrs().forEach(attr => {
72 if (this.attributes[attr.attr()]) {
74 if (this.attributes[attr.attr()].indexOf(attr.value()) < 0) {
75 this.attributes[attr.attr()].push(attr.value());
78 this.attributes[attr.attr()] = [attr.value()];
83 // Get -> Set -> Return bib hold count
84 getHoldCount(): Promise<number> {
86 if (Number.isInteger(this.holdCount)) {
87 return Promise.resolve(this.holdCount);
90 let method = 'open-ils.circ.bre.holds.count';
94 method = 'open-ils.circ.mmr.holds.count';
95 target = this.metabibId;
98 return this.net.request(
99 'open-ils.circ', method, target
100 ).toPromise().then(count => this.holdCount = count);
103 // Get -> Set -> Return bib-level call number
104 getBibCallNumber(): Promise<string> {
106 if (this.bibCallNumber !== null) {
107 return Promise.resolve(this.bibCallNumber);
110 // TODO labelClass = cat.default_classification_scheme YAOUS
111 const labelClass = 1;
113 return this.net.request(
115 'open-ils.cat.biblio.record.marc_cn.retrieve',
117 ).toPromise().then(cnArray => {
118 if (cnArray && cnArray.length > 0) {
119 const key1 = Object.keys(cnArray[0])[0];
120 this.bibCallNumber = cnArray[0][key1];
122 this.bibCallNumber = '';
124 return this.bibCallNumber;
130 export class BibRecordService {
132 // Cache of bib editor / creator objects
133 // Assumption is this list will be limited in size.
134 userCache: {[id: number]: IdlObject};
137 private idl: IdlService,
138 private net: NetService,
139 private org: OrgService,
140 private unapi: UnapiService,
141 private pcrud: PcrudService
146 // Avoid fetching the MARC blob by specifying which fields on the
147 // bre to select. Note that fleshed fields are implicitly selected.
148 fetchableBreFields(): string[] {
149 return this.idl.classes.bre.fields
150 .filter(f => !f.virtual && f.name !== 'marc')
154 // Note when multiple IDs are provided, responses are emitted in order
155 // of receipt, not necessarily in the requested ID order.
156 getBibSummary(bibIds: number | number[],
157 orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
159 const ids = [].concat(bibIds);
161 if (ids.length === 0) {
165 return this.pcrud.search('bre', {id: ids},
167 flesh_fields: {bre: ['flat_display_entries', 'mattrs']},
168 select: {bre : this.fetchableBreFields()}
170 {anonymous: true} // skip unneccesary auth
171 ).pipe(mergeMap(bib => {
172 const summary = new BibRecordSummary(bib, orgId, orgDepth);
173 summary.net = this.net; // inject
175 return this.getHoldingsSummary(bib.id(), orgId, orgDepth)
176 .then(holdingsSummary => {
177 summary.holdingsSummary = holdingsSummary;
183 // A Metabib Summary is a BibRecordSummary with the lead record as
184 // its core bib record plus attributes (e.g. formats) from related
186 getMetabibSummary(metabibIds: number | number[],
187 orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
189 const ids = [].concat(metabibIds);
191 if (ids.length === 0) {
195 return this.pcrud.search('mmr', {id: ids},
196 {flesh: 1, flesh_fields: {mmr: ['source_maps']}},
198 ).pipe(mergeMap(mmr => this.compileMetabib(mmr, orgId, orgDepth)));
201 // 'metabib' must have its "source_maps" field fleshed.
202 // Get bib summaries for all related bib records so we can
203 // extract data that must be appended to the master record summary.
204 compileMetabib(metabib: IdlObject,
205 orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
207 // TODO: Create an API similar to the one that builds a combined
208 // mods blob for metarecords, except using display fields, etc.
209 // For now, this seems to get the job done.
211 // Non-master records
212 const relatedBibIds = metabib.source_maps()
213 .map(m => m.source())
214 .filter(id => id !== metabib.master_record());
217 const observable = new Observable<BibRecordSummary>(o => observer = o);
219 // NOTE: getBibSummary calls getHoldingsSummary against
220 // the bib record unnecessarily. It's called again below.
221 // Reconsider this approach (see also note above about API).
222 this.getBibSummary(metabib.master_record(), orgId, orgDepth)
223 .subscribe(summary => {
224 summary.metabibId = Number(metabib.id());
225 summary.metabibRecords =
226 metabib.source_maps().map(m => Number(m.source()));
230 if (relatedBibIds.length > 0) {
232 // Grab data for MR bib summary augmentation
233 promise = this.pcrud.search('mraf', {id: relatedBibIds})
234 .pipe(tap(attr => summary.record.mattrs().push(attr)))
238 // Metarecord has only one constituent bib.
239 promise = Promise.resolve();
244 // Re-compile with augmented data
245 summary.compileRecordAttrs();
247 // Fetch holdings data for the metarecord
248 this.getHoldingsSummary(metabib.id(), orgId, orgDepth, true)
249 .then(holdingsSummary => {
250 summary.holdingsSummary = holdingsSummary;
251 observer.next(summary);
260 // Flesh the creator and editor fields.
261 // Handling this separately lets us pull from the cache and
262 // avoids the requirement that the main bib query use a staff
263 // (VIEW_USER) auth token.
264 fleshBibUsers(records: IdlObject[]): Promise<void> {
268 records.forEach(rec => {
269 ['creator', 'editor'].forEach(field => {
270 const id = rec[field]();
271 if (Number.isInteger(id)) {
272 if (this.userCache[id]) {
273 rec[field](this.userCache[id]);
274 } else if (!search.includes(id)) {
281 if (search.length === 0) {
282 return Promise.resolve();
285 return this.pcrud.search('au', {id: search})
287 this.userCache[user.id()] = user;
288 records.forEach(rec => {
289 if (user.id() === rec.creator()) {
292 if (user.id() === rec.editor()) {
299 getHoldingsSummary(recordId: number,
300 orgId: number, orgDepth: number, isMetarecord?: boolean): Promise<any> {
302 const holdingsSummary = [];
304 return this.unapi.getAsXmlDocument({
305 target: isMetarecord ? 'mmr' : 'bre',
307 extras: '{holdings_xml}',
308 format: 'holdings_xml',
313 // namespace resolver
314 const resolver: any = (prefix: string): string => {
315 return NAMESPACE_MAPS[prefix] || null;
318 // Extract the holdings data from the unapi xml doc
319 const result = xmlDoc.evaluate(HOLDINGS_XPATH,
320 xmlDoc, resolver, XPathResult.ANY_TYPE, null);
323 while (node = result.iterateNext()) {
324 const counts = {type : node.getAttribute('type')};
325 ['depth', 'org_unit', 'transcendant',
326 'available', 'count', 'unshadow'].forEach(field => {
327 counts[field] = Number(node.getAttribute(field));
329 holdingsSummary.push(counts);
332 return holdingsSummary;