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';
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
30 bibCallNumber: string;
33 constructor(record: IdlObject, orgId: number, orgDepth: number) {
34 this.id = record.id();
37 this.orgDepth = orgDepth;
40 this.bibCallNumber = null;
44 this.compileDisplayFields();
45 this.compileRecordAttrs();
47 // Normalize some data for JS consistency
48 this.record.creator(Number(this.record.creator()));
49 this.record.editor(Number(this.record.editor()));
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());
58 this.display[entry.name()] = [entry.value()];
61 this.display[entry.name()] = entry.value();
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());
72 this.attributes[attr.attr()] = [attr.value()];
77 // Get -> Set -> Return bib hold count
78 getHoldCount(): Promise<number> {
80 if (Number.isInteger(this.holdCount)) {
81 return Promise.resolve(this.holdCount);
84 return this.net.request(
86 'open-ils.circ.bre.holds.count', this.id
87 ).toPromise().then(count => this.holdCount = count);
90 // Get -> Set -> Return bib-level call number
91 getBibCallNumber(): Promise<string> {
93 if (this.bibCallNumber !== null) {
94 return Promise.resolve(this.bibCallNumber);
97 // TODO labelClass = cat.default_classification_scheme YAOUS
100 return this.net.request(
102 'open-ils.cat.biblio.record.marc_cn.retrieve',
104 ).toPromise().then(cnArray => {
105 if (cnArray && cnArray.length > 0) {
106 const key1 = Object.keys(cnArray[0])[0];
107 this.bibCallNumber = cnArray[0][key1];
109 this.bibCallNumber = '';
111 return this.bibCallNumber;
117 export class BibRecordService {
119 // Cache of bib editor / creator objects
120 // Assumption is this list will be limited in size.
121 userCache: {[id: number]: IdlObject};
124 private idl: IdlService,
125 private net: NetService,
126 private org: OrgService,
127 private unapi: UnapiService,
128 private pcrud: PcrudService
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')
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> {
146 const ids = [].concat(bibIds);
148 if (ids.length === 0) {
152 return this.pcrud.search('bre', {id: ids},
154 flesh_fields: {bre: ['flat_display_entries', 'mattrs']},
155 select: {bre : this.fetchableBreFields()}
157 {anonymous: true} // skip unneccesary auth
158 ).pipe(mergeMap(bib => {
159 const summary = new BibRecordSummary(bib, orgId, orgDepth);
160 summary.net = this.net; // inject
162 return this.getHoldingsSummary(bib.id(), orgId, orgDepth)
163 .then(holdingsSummary => {
164 summary.holdingsSummary = holdingsSummary;
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> {
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)) {
191 if (search.length === 0) {
192 return Promise.resolve();
195 return this.pcrud.search('au', {id: search})
197 this.userCache[user.id()] = user;
198 records.forEach(rec => {
199 if (user.id() === rec.creator()) {
202 if (user.id() === rec.editor()) {
209 getHoldingsSummary(recordId: number,
210 orgId: number, orgDepth: number): Promise<any> {
212 const holdingsSummary = [];
214 return this.unapi.getAsXmlDocument({
217 extras: '{holdings_xml}',
218 format: 'holdings_xml',
223 // namespace resolver
224 const resolver: any = (prefix: string): string => {
225 return NAMESPACE_MAPS[prefix] || null;
228 // Extract the holdings data from the unapi xml doc
229 const result = xmlDoc.evaluate(HOLDINGS_XPATH,
230 xmlDoc, resolver, XPathResult.ANY_TYPE, null);
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));
239 holdingsSummary.push(counts);
242 return holdingsSummary;