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;
34 displayHighlights: {[name: string]: string | string[]} = {};
36 constructor(record: IdlObject, orgId: number, orgDepth: number) {
37 this.id = Number(record.id());
40 this.orgDepth = orgDepth;
43 this.bibCallNumber = null;
44 this.metabibRecords = [];
48 this.compileDisplayFields();
49 this.compileRecordAttrs();
51 // Normalize some data for JS consistency
52 this.record.creator(Number(this.record.creator()));
53 this.record.editor(Number(this.record.editor()));
56 compileDisplayFields() {
57 this.record.flat_display_entries().forEach(entry => {
58 if (entry.multi() === 't') {
59 if (this.display[entry.name()]) {
60 this.display[entry.name()].push(entry.value());
62 this.display[entry.name()] = [entry.value()];
65 this.display[entry.name()] = entry.value();
70 compileRecordAttrs() {
71 // Any attr can be multi-valued.
72 this.record.mattrs().forEach(attr => {
73 if (this.attributes[attr.attr()]) {
75 if (this.attributes[attr.attr()].indexOf(attr.value()) < 0) {
76 this.attributes[attr.attr()].push(attr.value());
79 this.attributes[attr.attr()] = [attr.value()];
84 // Get -> Set -> Return bib hold count
85 getHoldCount(): Promise<number> {
87 if (Number.isInteger(this.holdCount)) {
88 return Promise.resolve(this.holdCount);
91 let method = 'open-ils.circ.bre.holds.count';
95 method = 'open-ils.circ.mmr.holds.count';
96 target = this.metabibId;
99 return this.net.request(
100 'open-ils.circ', method, target
101 ).toPromise().then(count => this.holdCount = count);
104 // Get -> Set -> Return bib-level call number
105 getBibCallNumber(): Promise<string> {
107 if (this.bibCallNumber !== null) {
108 return Promise.resolve(this.bibCallNumber);
111 // TODO labelClass = cat.default_classification_scheme YAOUS
112 const labelClass = 1;
114 return this.net.request(
116 'open-ils.cat.biblio.record.marc_cn.retrieve',
118 ).toPromise().then(cnArray => {
119 if (cnArray && cnArray.length > 0) {
120 const key1 = Object.keys(cnArray[0])[0];
121 this.bibCallNumber = cnArray[0][key1];
123 this.bibCallNumber = '';
125 return this.bibCallNumber;
131 export class BibRecordService {
133 // Cache of bib editor / creator objects
134 // Assumption is this list will be limited in size.
135 userCache: {[id: number]: IdlObject};
138 private idl: IdlService,
139 private net: NetService,
140 private org: OrgService,
141 private unapi: UnapiService,
142 private pcrud: PcrudService
147 // Avoid fetching the MARC blob by specifying which fields on the
148 // bre to select. Note that fleshed fields are implicitly selected.
149 fetchableBreFields(): string[] {
150 return this.idl.classes.bre.fields
151 .filter(f => !f.virtual && f.name !== 'marc')
155 // Note when multiple IDs are provided, responses are emitted in order
156 // of receipt, not necessarily in the requested ID order.
157 getBibSummary(bibIds: number | number[],
158 orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
160 const ids = [].concat(bibIds);
162 if (ids.length === 0) {
166 return this.pcrud.search('bre', {id: ids},
168 flesh_fields: {bre: ['flat_display_entries', 'mattrs']},
169 select: {bre : this.fetchableBreFields()}
171 {anonymous: true} // skip unneccesary auth
172 ).pipe(mergeMap(bib => {
173 const summary = new BibRecordSummary(bib, orgId, orgDepth);
174 summary.net = this.net; // inject
176 return this.getHoldingsSummary(bib.id(), orgId, orgDepth)
177 .then(holdingsSummary => {
178 summary.holdingsSummary = holdingsSummary;
184 // A Metabib Summary is a BibRecordSummary with the lead record as
185 // its core bib record plus attributes (e.g. formats) from related
187 getMetabibSummary(metabibIds: number | number[],
188 orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
190 const ids = [].concat(metabibIds);
192 if (ids.length === 0) {
196 return this.pcrud.search('mmr', {id: ids},
197 {flesh: 1, flesh_fields: {mmr: ['source_maps']}},
199 ).pipe(mergeMap(mmr => this.compileMetabib(mmr, orgId, orgDepth)));
202 // 'metabib' must have its "source_maps" field fleshed.
203 // Get bib summaries for all related bib records so we can
204 // extract data that must be appended to the master record summary.
205 compileMetabib(metabib: IdlObject,
206 orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
208 // TODO: Create an API similar to the one that builds a combined
209 // mods blob for metarecords, except using display fields, etc.
210 // For now, this seems to get the job done.
212 // Non-master records
213 const relatedBibIds = metabib.source_maps()
214 .map(m => m.source())
215 .filter(id => id !== metabib.master_record());
218 const observable = new Observable<BibRecordSummary>(o => observer = o);
220 // NOTE: getBibSummary calls getHoldingsSummary against
221 // the bib record unnecessarily. It's called again below.
222 // Reconsider this approach (see also note above about API).
223 this.getBibSummary(metabib.master_record(), orgId, orgDepth)
224 .subscribe(summary => {
225 summary.metabibId = Number(metabib.id());
226 summary.metabibRecords =
227 metabib.source_maps().map(m => Number(m.source()));
231 if (relatedBibIds.length > 0) {
233 // Grab data for MR bib summary augmentation
234 promise = this.pcrud.search('mraf', {id: relatedBibIds})
235 .pipe(tap(attr => summary.record.mattrs().push(attr)))
239 // Metarecord has only one constituent bib.
240 promise = Promise.resolve();
245 // Re-compile with augmented data
246 summary.compileRecordAttrs();
248 // Fetch holdings data for the metarecord
249 this.getHoldingsSummary(metabib.id(), orgId, orgDepth, true)
250 .then(holdingsSummary => {
251 summary.holdingsSummary = holdingsSummary;
252 observer.next(summary);
261 // Flesh the creator and editor fields.
262 // Handling this separately lets us pull from the cache and
263 // avoids the requirement that the main bib query use a staff
264 // (VIEW_USER) auth token.
265 fleshBibUsers(records: IdlObject[]): Promise<void> {
269 records.forEach(rec => {
270 ['creator', 'editor'].forEach(field => {
271 const id = rec[field]();
272 if (Number.isInteger(id)) {
273 if (this.userCache[id]) {
274 rec[field](this.userCache[id]);
275 } else if (!search.includes(id)) {
282 if (search.length === 0) {
283 return Promise.resolve();
286 return this.pcrud.search('au', {id: search})
288 this.userCache[user.id()] = user;
289 records.forEach(rec => {
290 if (user.id() === rec.creator()) {
293 if (user.id() === rec.editor()) {
300 getHoldingsSummary(recordId: number,
301 orgId: number, orgDepth: number, isMetarecord?: boolean): Promise<any> {
303 const holdingsSummary = [];
305 return this.unapi.getAsXmlDocument({
306 target: isMetarecord ? 'mmr' : 'bre',
308 extras: '{holdings_xml}',
309 format: 'holdings_xml',
314 // namespace resolver
315 const resolver: any = (prefix: string): string => {
316 return NAMESPACE_MAPS[prefix] || null;
319 // Extract the holdings data from the unapi xml doc
320 const result = xmlDoc.evaluate(HOLDINGS_XPATH,
321 xmlDoc, resolver, XPathResult.ANY_TYPE, null);
324 while (node = result.iterateNext()) {
325 const counts = {type : node.getAttribute('type')};
326 ['depth', 'org_unit', 'transcendant',
327 'available', 'count', 'unshadow'].forEach(field => {
328 counts[field] = Number(node.getAttribute(field));
330 holdingsSummary.push(counts);
333 return holdingsSummary;