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';
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'
19 export const HOLDINGS_XPATH =
20 '/holdings:holdings/holdings:counts/holdings:count';
23 export class BibRecordSummary {
24 id: number; // == record.id() for convenience
32 bibCallNumber: string;
35 constructor(record: IdlObject, orgId: number, orgDepth: number) {
36 this.id = record.id();
39 this.orgDepth = orgDepth;
42 this.bibCallNumber = null;
46 this.compileDisplayFields();
47 this.compileRecordAttrs();
49 // Normalize some data for JS consistency
50 this.record.creator(Number(this.record.creator()));
51 this.record.editor(Number(this.record.editor()));
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());
60 this.display[entry.name()] = [entry.value()];
63 this.display[entry.name()] = entry.value();
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());
74 this.attributes[attr.attr()] = [attr.value()];
79 // Get -> Set -> Return bib hold count
80 getHoldCount(): Promise<number> {
82 if (Number.isInteger(this.holdCount)) {
83 return Promise.resolve(this.holdCount);
86 return this.net.request(
88 'open-ils.circ.bre.holds.count', this.id
89 ).toPromise().then(count => this.holdCount = count);
92 // Get -> Set -> Return bib-level call number
93 getBibCallNumber(): Promise<string> {
95 if (this.bibCallNumber !== null) {
96 return Promise.resolve(this.bibCallNumber);
99 // TODO labelClass = cat.default_classification_scheme YAOUS
100 const labelClass = 1;
102 return this.net.request(
104 'open-ils.cat.biblio.record.marc_cn.retrieve',
106 ).toPromise().then(cnArray => {
107 if (cnArray && cnArray.length > 0) {
108 const key1 = Object.keys(cnArray[0])[0];
109 this.bibCallNumber = cnArray[0][key1];
111 this.bibCallNumber = '';
113 return this.bibCallNumber;
119 export class BibRecordService {
121 // Cache of bib editor / creator objects
122 // Assumption is this list will be limited in size.
123 userCache: {[id: number]: IdlObject};
126 private idl: IdlService,
127 private net: NetService,
128 private org: OrgService,
129 private unapi: UnapiService,
130 private pcrud: PcrudService
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')
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> {
148 const ids = [].concat(bibIds);
150 if (ids.length === 0) {
154 return this.pcrud.search('bre', {id: ids},
156 flesh_fields: {bre: ['flat_display_entries', 'mattrs']},
157 select: {bre : this.fetchableBreFields()}
159 {anonymous: true} // skip unneccesary auth
160 ).pipe(mergeMap(bib => {
161 const summary = new BibRecordSummary(bib, orgId, orgDepth);
162 summary.net = this.net; // inject
164 return this.getHoldingsSummary(bib.id(), orgId, orgDepth)
165 .then(holdingsSummary => {
166 summary.holdingsSummary = holdingsSummary;
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> {
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)) {
193 if (search.length === 0) {
194 return Promise.resolve();
197 return this.pcrud.search('au', {id: search})
199 this.userCache[user.id()] = user;
200 records.forEach(rec => {
201 if (user.id() === rec.creator()) {
204 if (user.id() === rec.editor()) {
211 getHoldingsSummary(recordId: number,
212 orgId: number, orgDepth: number): Promise<any> {
214 const holdingsSummary = [];
216 return this.unapi.getAsXmlDocument({
219 extras: '{holdings_xml}',
220 format: 'holdings_xml',
225 // namespace resolver
226 const resolver: any = (prefix: string): string => {
227 return NAMESPACE_MAPS[prefix] || null;
230 // Extract the holdings data from the unapi xml doc
231 const result = xmlDoc.evaluate(HOLDINGS_XPATH,
232 xmlDoc, resolver, XPathResult.ANY_TYPE, null);
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));
241 holdingsSummary.push(counts);
244 return holdingsSummary;