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 return this.net.request(
113 'open-ils.cat.biblio.record.marc_cn.retrieve',
114 this.id, null, this.orgId
115 ).toPromise().then(cnArray => {
116 if (cnArray && cnArray.length > 0) {
117 const key1 = Object.keys(cnArray[0])[0];
118 this.bibCallNumber = cnArray[0][key1];
120 this.bibCallNumber = '';
122 return this.bibCallNumber;
128 export class BibRecordService {
130 // Cache of bib editor / creator objects
131 // Assumption is this list will be limited in size.
132 userCache: {[id: number]: IdlObject};
135 private idl: IdlService,
136 private net: NetService,
137 private org: OrgService,
138 private unapi: UnapiService,
139 private pcrud: PcrudService
144 // Avoid fetching the MARC blob by specifying which fields on the
145 // bre to select. Note that fleshed fields are implicitly selected.
146 fetchableBreFields(): string[] {
147 return this.idl.classes.bre.fields
148 .filter(f => !f.virtual && f.name !== 'marc')
152 // Note when multiple IDs are provided, responses are emitted in order
153 // of receipt, not necessarily in the requested ID order.
154 getBibSummary(bibIds: number | number[],
155 orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
157 const ids = [].concat(bibIds);
159 if (ids.length === 0) {
163 return this.pcrud.search('bre', {id: ids},
165 flesh_fields: {bre: ['flat_display_entries', 'mattrs']},
166 select: {bre : this.fetchableBreFields()}
168 {anonymous: true} // skip unneccesary auth
169 ).pipe(mergeMap(bib => {
170 const summary = new BibRecordSummary(bib, orgId, orgDepth);
171 summary.net = this.net; // inject
173 return this.getHoldingsSummary(bib.id(), orgId, orgDepth)
174 .then(holdingsSummary => {
175 summary.holdingsSummary = holdingsSummary;
181 // A Metabib Summary is a BibRecordSummary with the lead record as
182 // its core bib record plus attributes (e.g. formats) from related
184 getMetabibSummary(metabibIds: number | number[],
185 orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
187 const ids = [].concat(metabibIds);
189 if (ids.length === 0) {
193 return this.pcrud.search('mmr', {id: ids},
194 {flesh: 1, flesh_fields: {mmr: ['source_maps']}},
196 ).pipe(mergeMap(mmr => this.compileMetabib(mmr, orgId, orgDepth)));
199 // 'metabib' must have its "source_maps" field fleshed.
200 // Get bib summaries for all related bib records so we can
201 // extract data that must be appended to the master record summary.
202 compileMetabib(metabib: IdlObject,
203 orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
205 // TODO: Create an API similar to the one that builds a combined
206 // mods blob for metarecords, except using display fields, etc.
207 // For now, this seems to get the job done.
209 // Non-master records
210 const relatedBibIds = metabib.source_maps()
211 .map(m => m.source())
212 .filter(id => id !== metabib.master_record());
215 const observable = new Observable<BibRecordSummary>(o => observer = o);
217 // NOTE: getBibSummary calls getHoldingsSummary against
218 // the bib record unnecessarily. It's called again below.
219 // Reconsider this approach (see also note above about API).
220 this.getBibSummary(metabib.master_record(), orgId, orgDepth)
221 .subscribe(summary => {
222 summary.metabibId = Number(metabib.id());
223 summary.metabibRecords =
224 metabib.source_maps().map(m => Number(m.source()));
228 if (relatedBibIds.length > 0) {
230 // Grab data for MR bib summary augmentation
231 promise = this.pcrud.search('mraf', {id: relatedBibIds})
232 .pipe(tap(attr => summary.record.mattrs().push(attr)))
236 // Metarecord has only one constituent bib.
237 promise = Promise.resolve();
242 // Re-compile with augmented data
243 summary.compileRecordAttrs();
245 // Fetch holdings data for the metarecord
246 this.getHoldingsSummary(metabib.id(), orgId, orgDepth, true)
247 .then(holdingsSummary => {
248 summary.holdingsSummary = holdingsSummary;
249 observer.next(summary);
258 // Flesh the creator and editor fields.
259 // Handling this separately lets us pull from the cache and
260 // avoids the requirement that the main bib query use a staff
261 // (VIEW_USER) auth token.
262 fleshBibUsers(records: IdlObject[]): Promise<void> {
266 records.forEach(rec => {
267 ['creator', 'editor'].forEach(field => {
268 const id = rec[field]();
269 if (Number.isInteger(id)) {
270 if (this.userCache[id]) {
271 rec[field](this.userCache[id]);
272 } else if (!search.includes(id)) {
279 if (search.length === 0) {
280 return Promise.resolve();
283 return this.pcrud.search('au', {id: search})
285 this.userCache[user.id()] = user;
286 records.forEach(rec => {
287 if (user.id() === rec.creator()) {
290 if (user.id() === rec.editor()) {
297 getHoldingsSummary(recordId: number,
298 orgId: number, orgDepth: number, isMetarecord?: boolean): Promise<any> {
300 const holdingsSummary = [];
302 return this.unapi.getAsXmlDocument({
303 target: isMetarecord ? 'mmr' : 'bre',
305 extras: '{holdings_xml}',
306 format: 'holdings_xml',
311 // namespace resolver
312 const resolver: any = (prefix: string): string => {
313 return NAMESPACE_MAPS[prefix] || null;
316 // Extract the holdings data from the unapi xml doc
317 const result = xmlDoc.evaluate(HOLDINGS_XPATH,
318 xmlDoc, resolver, XPathResult.ANY_TYPE, null);
321 while (node = result.iterateNext()) {
322 const counts = {type : node.getAttribute('type')};
323 ['depth', 'org_unit', 'transcendant',
324 'available', 'count', 'unshadow'].forEach(field => {
325 counts[field] = Number(node.getAttribute(field));
327 holdingsSummary.push(counts);
330 return holdingsSummary;