1 import {Injectable, EventEmitter} from '@angular/core';
2 import {Observable} 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';
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);
54 return this.termSearch(ctx);
58 barcodeSearch(ctx: CatalogSearchContext): Promise<void> {
59 return this.net.request(
61 'open-ils.search.multi_home.bib_ids.by_barcode',
63 ).toPromise().then(ids => {
66 ids: ids.map(id => [id])
69 this.applyResultData(ctx, result);
70 ctx.searchState = CatalogSearchState.COMPLETE;
71 this.onSearchComplete.emit(ctx);
75 // "Search" the basket by loading the IDs and treating
76 // them like a standard query search results set.
77 basketSearch(ctx: CatalogSearchContext): Promise<void> {
79 return this.basket.getRecordIds().then(ids => {
81 // Map our list of IDs into a search results object
82 // the search context can understand.
85 ids: ids.map(id => [id])
88 this.applyResultData(ctx, result);
89 ctx.searchState = CatalogSearchState.COMPLETE;
90 this.onSearchComplete.emit(ctx);
94 marcSearch(ctx: CatalogSearchContext): Promise<void> {
95 let method = 'open-ils.search.biblio.marc';
96 if (ctx.isStaff) { method += '.staff'; }
98 const queryStruct = ctx.compileMarcSearchArgs();
100 return this.net.request('open-ils.search', method, queryStruct)
101 .toPromise().then(result => {
102 // Match the query search return format
103 result.ids = result.ids.map(id => [id]);
105 this.applyResultData(ctx, result);
106 ctx.searchState = CatalogSearchState.COMPLETE;
107 this.onSearchComplete.emit(ctx);
111 termSearch(ctx: CatalogSearchContext): Promise<void> {
113 let method = 'open-ils.search.biblio.multiclass.query';
116 if (ctx.identSearch.isSearchable()) {
117 fullQuery = ctx.compileIdentSearchQuery();
120 fullQuery = ctx.compileTermSearchQuery();
122 if (ctx.termSearch.groupByMetarecord
123 && !ctx.termSearch.fromMetarecord) {
124 method = 'open-ils.search.metabib.multiclass.query';
127 if (ctx.termSearch.hasBrowseEntry) {
128 this.fetchBrowseEntry(ctx);
132 console.debug(`search query: ${fullQuery}`);
138 return new Promise((resolve, reject) => {
140 'open-ils.search', method, {
141 limit : ctx.pager.limit + 1,
142 offset : ctx.pager.offset
144 ).subscribe(result => {
145 this.applyResultData(ctx, result);
146 ctx.searchState = CatalogSearchState.COMPLETE;
147 this.onSearchComplete.emit(ctx);
154 // When showing titles linked to a browse entry, fetch
155 // the entry data as well so the UI can display it.
156 fetchBrowseEntry(ctx: CatalogSearchContext) {
157 const ts = ctx.termSearch;
159 const parts = ts.hasBrowseEntry.split(',');
160 const mbeId = parts[0];
161 const cmfId = parts[1];
163 this.pcrud.retrieve('mbe', mbeId)
164 .subscribe(mbe => ctx.termSearch.browseEntry = mbe);
167 applyResultData(ctx: CatalogSearchContext, result: any): void {
169 ctx.pager.resultCount = result.count;
171 // records[] tracks the current page of bib summaries.
174 // If this is a new search, reset the result IDs collection.
175 if (this.lastFacetKey !== result.facet_key) {
179 result.ids.forEach((blob, idx) => ctx.addResultId(blob[0], idx));
182 // Appends records to the search result set as they arrive.
183 // Returns a void promise once all records have been retrieved
184 fetchBibSummaries(ctx: CatalogSearchContext): Promise<void> {
186 const depth = ctx.global ?
187 ctx.org.root().ou_type().depth() :
188 ctx.searchOrg.ou_type().depth();
190 const isMeta = ctx.termSearch.isMetarecordSearch();
192 let observable: Observable<BibRecordSummary>;
195 observable = this.bibService.getMetabibSummary(
196 ctx.currentResultIds(), ctx.searchOrg.id(), depth);
198 observable = this.bibService.getBibSummary(
199 ctx.currentResultIds(), ctx.searchOrg.id(), depth);
202 return observable.pipe(map(summary => {
203 // Responses are not necessarily returned in request-ID order.
206 idx = ctx.currentResultIds().indexOf(summary.metabibId);
208 idx = ctx.currentResultIds().indexOf(summary.id);
211 if (ctx.result.records) {
212 // May be reset when quickly navigating results.
213 ctx.result.records[idx] = summary;
218 fetchFacets(ctx: CatalogSearchContext): Promise<void> {
221 return Promise.reject('Cannot fetch facets without results');
224 if (!ctx.result.facet_key) {
225 return Promise.resolve();
228 if (this.lastFacetKey === ctx.result.facet_key) {
229 ctx.result.facetData = this.lastFacetData;
230 return Promise.resolve();
233 return new Promise((resolve, reject) => {
234 this.net.request('open-ils.search',
235 'open-ils.search.facet_cache.retrieve',
237 ).subscribe(facets => {
238 const facetData = {};
239 Object.keys(facets).forEach(cmfId => {
240 const facetHash = facets[cmfId];
241 const cmf = this.cmfMap[cmfId];
244 Object.keys(facetHash).forEach(value => {
245 const count = facetHash[value];
246 cmfData.push({value : value, count : count});
249 if (!facetData[cmf.field_class()]) {
250 facetData[cmf.field_class()] = {};
253 facetData[cmf.field_class()][cmf.name()] = {
254 cmfLabel : cmf.label(),
255 valueList : cmfData.sort((a, b) => {
256 if (a.count > b.count) { return -1; }
257 if (a.count < b.count) { return 1; }
258 // secondary alpha sort on display value
259 return a.value < b.value ? -1 : 1;
264 this.lastFacetKey = ctx.result.facet_key;
265 this.lastFacetData = ctx.result.facetData = facetData;
271 fetchCcvms(): Promise<void> {
273 if (Object.keys(this.ccvmMap).length) {
274 return Promise.resolve();
277 return new Promise((resolve, reject) => {
278 this.pcrud.search('ccvm',
279 {ctype : CATALOG_CCVM_FILTERS}, {},
280 {atomic: true, anonymous: true}
281 ).subscribe(list => {
282 this.compileCcvms(list);
288 compileCcvms(ccvms: IdlObject[]): void {
289 ccvms.forEach(ccvm => {
290 if (!this.ccvmMap[ccvm.ctype()]) {
291 this.ccvmMap[ccvm.ctype()] = [];
293 this.ccvmMap[ccvm.ctype()].push(ccvm);
296 Object.keys(this.ccvmMap).forEach(cType => {
297 this.ccvmMap[cType] =
298 this.ccvmMap[cType].sort((a, b) => {
299 return a.value() < b.value() ? -1 : 1;
304 iconFormatLabel(code: string): string {
306 const ccvm = this.ccvmMap.icon_format.filter(
307 format => format.code() === code)[0];
309 return ccvm.search_label();
314 fetchCmfs(): Promise<void> {
315 // At the moment, we only need facet CMFs.
316 if (Object.keys(this.cmfMap).length) {
317 return Promise.resolve();
320 return new Promise((resolve, reject) => {
321 this.pcrud.search('cmf',
322 {facet_field : 't'}, {}, {atomic: true, anonymous: true}
325 cmfs.forEach(c => this.cmfMap[c.id()] = c);
332 fetchCopyLocations(contextOrg: number | IdlObject): Promise<any> {
333 const orgIds = this.org.fullPath(contextOrg, true);
334 this.copyLocations = [];
336 return this.pcrud.search('acpl',
337 {deleted: 'f', opac_visible: 't', owning_lib: orgIds},
338 {order_by: {acpl: 'name'}},
340 ).pipe(tap(loc => this.copyLocations.push(loc))).toPromise();
343 browse(ctx: CatalogSearchContext): Observable<any> {
344 ctx.searchState = CatalogSearchState.SEARCHING;
345 const bs = ctx.browseSearch;
347 let method = 'open-ils.search.browse';
352 return this.net.request(
354 'open-ils.search.browse.staff', {
355 browse_class: bs.fieldClass,
357 limit : ctx.pager.limit,
359 org_unit: ctx.searchOrg.id()
361 ).pipe(tap(result => {
362 ctx.searchState = CatalogSearchState.COMPLETE;