1 import {Injectable, EventEmitter} from '@angular/core';
2 import {Observable} from 'rxjs';
3 import {map, tap, finalize} 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';
9 import {CatalogSearchContext, CatalogSearchState} from './search-context';
10 import {BibRecordService, BibRecordSummary} from './bib-record.service';
11 import {BasketService} from './basket.service';
12 import {CATALOG_CCVM_FILTERS} from './search-context';
15 export class CatalogService {
17 ccvmMap: {[ccvm: string]: IdlObject[]} = {};
18 cmfMap: {[cmf: string]: IdlObject} = {};
19 copyLocations: IdlObject[];
21 // Keep a reference to the most recently retrieved facet data,
22 // since facet data is consistent across a given search.
23 // No need to re-fetch with every page of search data.
27 // Allow anyone to watch for completed searches.
28 onSearchComplete: EventEmitter<CatalogSearchContext>;
31 private idl: IdlService,
32 private net: NetService,
33 private org: OrgService,
34 private unapi: UnapiService,
35 private pcrud: PcrudService,
36 private bibService: BibRecordService,
37 private basket: BasketService
39 this.onSearchComplete = new EventEmitter<CatalogSearchContext>();
43 search(ctx: CatalogSearchContext): Promise<void> {
44 ctx.searchState = CatalogSearchState.SEARCHING;
47 return this.basketSearch(ctx);
48 } else if (ctx.marcSearch.isSearchable()) {
49 return this.marcSearch(ctx);
50 } else if (ctx.identSearch.isSearchable() &&
51 ctx.identSearch.queryType === 'item_barcode') {
52 return this.barcodeSearch(ctx);
55 ctx.identSearch.isSearchable() &&
56 ctx.identSearch.queryType === 'identifier|tcn') {
57 return this.tcnStaffSearch(ctx);
59 return this.termSearch(ctx);
63 barcodeSearch(ctx: CatalogSearchContext): Promise<void> {
64 return this.net.request(
66 'open-ils.search.multi_home.bib_ids.by_barcode',
68 ).toPromise().then(ids => {
69 // API returns an event for not-found barcodes
70 if (!Array.isArray(ids)) { ids = []; }
73 ids: ids.map(id => [id])
76 this.applyResultData(ctx, result);
77 ctx.searchState = CatalogSearchState.COMPLETE;
78 this.onSearchComplete.emit(ctx);
82 tcnStaffSearch(ctx: CatalogSearchContext): Promise<void> {
83 return this.net.request(
85 'open-ils.search.biblio.tcn',
86 ctx.identSearch.value, 1
87 ).toPromise().then(result => {
88 result.ids = result.ids.map(id => [id]);
89 this.applyResultData(ctx, result);
90 ctx.searchState = CatalogSearchState.COMPLETE;
91 this.onSearchComplete.emit(ctx);
96 // "Search" the basket by loading the IDs and treating
97 // them like a standard query search results set.
98 basketSearch(ctx: CatalogSearchContext): Promise<void> {
100 return this.basket.getRecordIds().then(ids => {
103 ids.slice(ctx.pager.offset, ctx.pager.limit + ctx.pager.offset);
105 // Map our list of IDs into a search results object
106 // the search context can understand.
109 ids: pageIds.map(id => [id])
112 this.applyResultData(ctx, result);
113 ctx.searchState = CatalogSearchState.COMPLETE;
114 this.onSearchComplete.emit(ctx);
118 marcSearch(ctx: CatalogSearchContext): Promise<void> {
119 let method = 'open-ils.search.biblio.marc';
120 if (ctx.isStaff) { method += '.staff'; }
122 const queryStruct = ctx.compileMarcSearchArgs();
124 return this.net.request('open-ils.search', method, queryStruct)
125 .toPromise().then(result => {
126 // Match the query search return format
127 result.ids = result.ids.map(id => [id]);
129 this.applyResultData(ctx, result);
130 ctx.searchState = CatalogSearchState.COMPLETE;
131 this.onSearchComplete.emit(ctx);
135 termSearch(ctx: CatalogSearchContext): Promise<void> {
137 let method = 'open-ils.search.biblio.multiclass.query';
140 if (ctx.identSearch.isSearchable()) {
141 fullQuery = ctx.compileIdentSearchQuery();
144 fullQuery = ctx.compileTermSearchQuery();
146 if (ctx.termSearch.groupByMetarecord
147 && !ctx.termSearch.fromMetarecord) {
148 method = 'open-ils.search.metabib.multiclass.query';
151 if (ctx.termSearch.hasBrowseEntry) {
152 this.fetchBrowseEntry(ctx);
156 console.debug(`search query: ${fullQuery}`);
162 return this.net.request(
163 'open-ils.search', method, {
164 limit : ctx.pager.limit + 1,
165 offset : ctx.pager.offset
168 .then(result => this.applyResultData(ctx, result))
169 .then(_ => this.fetchFieldHighlights(ctx))
171 ctx.searchState = CatalogSearchState.COMPLETE;
172 this.onSearchComplete.emit(ctx);
176 // When showing titles linked to a browse entry, fetch
177 // the entry data as well so the UI can display it.
178 fetchBrowseEntry(ctx: CatalogSearchContext) {
179 const ts = ctx.termSearch;
181 const parts = ts.hasBrowseEntry.split(',');
182 const mbeId = parts[0];
183 const cmfId = parts[1];
185 this.pcrud.retrieve('mbe', mbeId)
186 .subscribe(mbe => ctx.termSearch.browseEntry = mbe);
189 applyResultData(ctx: CatalogSearchContext, result: any): void {
191 ctx.pager.resultCount = result.count;
193 // records[] tracks the current page of bib summaries.
196 // If this is a new search, reset the result IDs collection.
197 if (this.lastFacetKey !== result.facet_key) {
201 result.ids.forEach((blob, idx) => ctx.addResultId(blob[0], idx));
204 // Appends records to the search result set as they arrive.
205 // Returns a void promise once all records have been retrieved
206 fetchBibSummaries(ctx: CatalogSearchContext): Promise<void> {
208 const org = ctx.global ? ctx.org.root() : ctx.searchOrg;
209 const depth = org.ou_type().depth();
211 const isMeta = ctx.termSearch.isMetarecordSearch();
213 let observable: Observable<BibRecordSummary>;
215 const options: any = {pref_ou: ctx.prefOu};
217 if (ctx.showResultExtras) {
218 options.flesh_copies = true;
219 options.copy_depth = depth;
220 options.copy_limit = 5;
221 options.pref_ou = ctx.prefOu;
225 observable = this.bibService.getMetabibSummaries(
226 ctx.currentResultIds(), ctx.searchOrg.id(), ctx.isStaff, options);
228 observable = this.bibService.getBibSummaries(
229 ctx.currentResultIds(), ctx.searchOrg.id(), ctx.isStaff, options);
232 return observable.pipe(map(summary => {
233 // Responses are not necessarily returned in request-ID order.
236 idx = ctx.currentResultIds().indexOf(summary.metabibId);
238 idx = ctx.currentResultIds().indexOf(summary.id);
241 if (ctx.result.records) {
242 // May be reset when quickly navigating results.
243 ctx.result.records[idx] = summary;
246 if (ctx.highlightData[summary.id]) {
247 summary.displayHighlights = ctx.highlightData[summary.id];
252 fetchFieldHighlights(ctx: CatalogSearchContext): Promise<any> {
256 // Extract the highlight map. Not all searches have them.
257 if ((hlMap = ctx.result) &&
258 (hlMap = hlMap.global_summary) &&
259 (hlMap = hlMap.query_struct) &&
260 (hlMap = hlMap.additional_data) &&
261 (hlMap = hlMap.highlight_map) &&
262 (Object.keys(hlMap).length > 0)) {
263 } else { return Promise.resolve(); }
266 if (ctx.getHighlightsFor) {
267 ids = [ctx.getHighlightsFor];
269 // ctx.currentResultIds() returns bib IDs or metabib IDs
270 // depending on the search type. If we have metabib IDs, map
271 // them to bib IDs for highlighting.
272 ids = ctx.currentResultIds();
273 if (ctx.termSearch.groupByMetarecord) {
274 // The 4th slot in the result ID reports the master record
275 // for the metarecord in question. Sometimes it's null?
276 ids = ctx.result.ids.map(id => id[4]).filter(id => id !== null);
280 return this.net.requestWithParamList( // API is list-based
282 'open-ils.search.fetch.metabib.display_field.highlight',
284 ).pipe(map(fields => {
286 if (fields.length === 0) { return; }
288 // Each 'fields' collection is an array of display field
289 // values whose text is augmented with highlighting markup.
290 const highlights = ctx.highlightData[fields[0].source] = {};
292 fields.forEach(field => {
293 const dfMap = this.cmfMap[field.field].display_field_map();
294 if (!dfMap) { return; } // pretty sure this can't happen.
296 if (dfMap.multi() === 't') {
297 if (!highlights[dfMap.name()]) {
298 highlights[dfMap.name()] = [];
300 (highlights[dfMap.name()] as string[]).push(field.highlight);
302 highlights[dfMap.name()] = field.highlight;
309 fetchFacets(ctx: CatalogSearchContext): Promise<void> {
312 return Promise.reject('Cannot fetch facets without results');
315 if (!ctx.result.facet_key) {
316 return Promise.resolve();
319 if (this.lastFacetKey === ctx.result.facet_key) {
320 ctx.result.facetData = this.lastFacetData;
321 return Promise.resolve();
324 return new Promise((resolve, reject) => {
325 this.net.request('open-ils.search',
326 'open-ils.search.facet_cache.retrieve',
328 ).subscribe(facets => {
329 const facetData = {};
330 Object.keys(facets).forEach(cmfId => {
331 const facetHash = facets[cmfId];
332 const cmf = this.cmfMap[cmfId];
335 Object.keys(facetHash).forEach(value => {
336 const count = facetHash[value];
337 cmfData.push({value : value, count : count});
340 if (!facetData[cmf.field_class()]) {
341 facetData[cmf.field_class()] = {};
344 facetData[cmf.field_class()][cmf.name()] = {
345 cmfLabel : cmf.label(),
346 valueList : cmfData.sort((a, b) => {
347 if (a.count > b.count) { return -1; }
348 if (a.count < b.count) { return 1; }
349 // secondary alpha sort on display value
350 return a.value < b.value ? -1 : 1;
355 this.lastFacetKey = ctx.result.facet_key;
356 this.lastFacetData = ctx.result.facetData = facetData;
362 fetchCcvms(): Promise<void> {
364 if (Object.keys(this.ccvmMap).length) {
365 return Promise.resolve();
368 return new Promise((resolve, reject) => {
369 this.pcrud.search('ccvm',
370 {ctype : CATALOG_CCVM_FILTERS}, {},
371 {atomic: true, anonymous: true}
372 ).subscribe(list => {
373 this.compileCcvms(list);
379 compileCcvms(ccvms: IdlObject[]): void {
380 ccvms.forEach(ccvm => {
381 if (!this.ccvmMap[ccvm.ctype()]) {
382 this.ccvmMap[ccvm.ctype()] = [];
384 this.ccvmMap[ccvm.ctype()].push(ccvm);
387 Object.keys(this.ccvmMap).forEach(cType => {
388 this.ccvmMap[cType] =
389 this.ccvmMap[cType].sort((a, b) => {
390 return a.value() < b.value() ? -1 : 1;
395 iconFormatLabel(code: string): string {
396 if (this.ccvmMap && this.ccvmMap.icon_format) {
397 const ccvm = this.ccvmMap.icon_format.filter(
398 format => format.code() === code)[0];
400 return ccvm.search_label();
405 fetchCmfs(): Promise<void> {
406 if (Object.keys(this.cmfMap).length) {
407 return Promise.resolve();
410 return new Promise((resolve, reject) => {
411 this.pcrud.search('cmf',
412 {'-or': [{facet_field : 't'}, {display_field: 't'}]},
413 {flesh: 1, flesh_fields: {cmf: ['display_field_map']}},
414 {atomic: true, anonymous: true}
417 cmfs.forEach(c => this.cmfMap[c.id()] = c);
424 fetchCopyLocations(contextOrg: number | IdlObject): Promise<any> {
425 const contextOrgId: any = this.org.get(contextOrg).id();
427 // we ordinarily want the shelving locations associated with
428 // all ancestors and descendants of the context OU, but
429 // if the context OU is the root, we intentionally want
430 // only the ones owned by the root OU
431 const orgIds: any[] = contextOrgId === this.org.root().id()
433 : this.org.fullPath(contextOrg, true);
435 this.copyLocations = [];
437 return this.pcrud.search('acpl',
438 {deleted: 'f', opac_visible: 't', owning_lib: orgIds},
439 {order_by: {acpl: 'name'}},
441 ).pipe(tap(loc => this.copyLocations.push(loc))).toPromise();
444 browse(ctx: CatalogSearchContext): Observable<any> {
445 ctx.searchState = CatalogSearchState.SEARCHING;
446 const bs = ctx.browseSearch;
448 let method = 'open-ils.search.browse';
453 return this.net.request(
455 'open-ils.search.browse.staff', {
456 browse_class: bs.fieldClass,
458 limit : ctx.pager.limit,
460 org_unit: ctx.searchOrg.id()
463 tap(result => ctx.searchState = CatalogSearchState.COMPLETE),
464 finalize(() => this.onSearchComplete.emit(ctx))
468 cnBrowse(ctx: CatalogSearchContext): Observable<any> {
469 ctx.searchState = CatalogSearchState.SEARCHING;
470 const cbs = ctx.cnBrowseSearch;
472 return this.net.request(
474 'open-ils.supercat.call_number.browse',
475 cbs.value, ctx.searchOrg.shortname(), cbs.limit, cbs.offset
476 ).pipe(tap(result => ctx.searchState = CatalogSearchState.COMPLETE));