From c1b2e9b7606fdb04c626cbeb308508530694962a Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Fri, 31 Jul 2020 10:54:28 -0400 Subject: [PATCH] LP1889694 Staff catalog record summary API Replaces a number of result page and record detail page API calls with a bespoke API specifically created to return the data required for display bib and metabib record summary information in the catalog. Specifically, a single streaming API this replaces the following: * fleshed record retrieval ** including record display fields and attributes processing. * copy count retrieval * hold count retrieval The end result is 22 API calls per results page replaced with 2. Signed-off-by: Bill Erickson Signed-off-by: Galen Charlton --- .../app/share/catalog/bib-record.service.ts | 270 +++--------------- .../src/app/share/catalog/catalog.service.ts | 14 +- .../queued-record-matches.component.ts | 32 +-- .../catalog/cnbrowse/results.component.ts | 8 +- .../staff/catalog/record/record.component.ts | 1 - .../catalog/result/record.component.html | 16 +- .../staff/catalog/result/record.component.ts | 1 - .../catalog/result/results.component.html | 10 +- .../staff/catalog/result/results.component.ts | 30 +- .../staff/catalog/search-form.component.html | 2 +- .../bib-summary/bib-summary.component.ts | 1 - .../lib/OpenILS/Application/Search/Biblio.pm | 156 +++++++++- 12 files changed, 225 insertions(+), 316 deletions(-) diff --git a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts index d29c4bdef8..7510fe0d0b 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts @@ -33,7 +33,7 @@ export class BibRecordSummary { net: NetService; displayHighlights: {[name: string]: string | string[]} = {}; - constructor(record: IdlObject, orgId: number, orgDepth: number) { + constructor(record: IdlObject, orgId: number, orgDepth?: number) { this.id = Number(record.id()); this.record = record; this.orgId = orgId; @@ -44,63 +44,6 @@ export class BibRecordSummary { this.metabibRecords = []; } - ingest() { - this.compileDisplayFields(); - this.compileRecordAttrs(); - - // Normalize some data for JS consistency - this.record.creator(Number(this.record.creator())); - this.record.editor(Number(this.record.editor())); - } - - compileDisplayFields() { - this.record.flat_display_entries().forEach(entry => { - if (entry.multi() === 't') { - if (this.display[entry.name()]) { - this.display[entry.name()].push(entry.value()); - } else { - this.display[entry.name()] = [entry.value()]; - } - } else { - this.display[entry.name()] = entry.value(); - } - }); - } - - compileRecordAttrs() { - // Any attr can be multi-valued. - this.record.mattrs().forEach(attr => { - if (this.attributes[attr.attr()]) { - // Avoid dupes - if (this.attributes[attr.attr()].indexOf(attr.value()) < 0) { - this.attributes[attr.attr()].push(attr.value()); - } - } else { - this.attributes[attr.attr()] = [attr.value()]; - } - }); - } - - // Get -> Set -> Return bib hold count - getHoldCount(): Promise { - - if (Number.isInteger(this.holdCount)) { - return Promise.resolve(this.holdCount); - } - - let method = 'open-ils.circ.bre.holds.count'; - let target = this.id; - - if (this.metabibId) { - method = 'open-ils.circ.mmr.holds.count'; - target = this.metabibId; - } - - return this.net.request( - 'open-ils.circ', method, target - ).toPromise().then(count => this.holdCount = count); - } - // Get -> Set -> Return bib-level call number getBibCallNumber(): Promise { @@ -141,194 +84,53 @@ export class BibRecordService { this.userCache = {}; } - // Avoid fetching the MARC blob by specifying which fields on the - // bre to select. Note that fleshed fields are implicitly selected. - fetchableBreFields(): string[] { - return this.idl.classes.bre.fields - .filter(f => !f.virtual && f.name !== 'marc') - .map(f => f.name); + getBibSummary(id: number, + orgId?: number, isStaff?: boolean): Observable { + return this.getBibSummaries([id], orgId, isStaff); } - // Note when multiple IDs are provided, responses are emitted in order - // of receipt, not necessarily in the requested ID order. - getBibSummary(bibIds: number | number[], - orgId?: number, orgDepth?: number): Observable { + getBibSummaries(bibIds: number[], + orgId?: number, isStaff?: boolean): Observable { - const ids = [].concat(bibIds); + if (bibIds.length === 0) { return from([]); } + if (!orgId) { orgId = this.org.root().id(); } - if (ids.length === 0) { - return from([]); - } + let method = 'open-ils.search.biblio.record.catalog_summary'; + if (isStaff) { method += '.staff'; } - return this.pcrud.search('bre', {id: ids}, - { flesh: 1, - flesh_fields: {bre: ['flat_display_entries', 'mattrs']}, - select: {bre : this.fetchableBreFields()} - }, - {anonymous: true} // skip unneccesary auth - ).pipe(mergeMap(bib => { - const summary = new BibRecordSummary(bib, orgId, orgDepth); + return this.net.request('open-ils.search', method, orgId, bibIds) + .pipe(map(bibSummary => { + const summary = new BibRecordSummary(bibSummary.record, orgId); summary.net = this.net; // inject - summary.ingest(); - return this.getHoldingsSummary(bib.id(), orgId, orgDepth) - .then(holdingsSummary => { - summary.holdingsSummary = holdingsSummary; - return summary; - }); + summary.display = bibSummary.display; + summary.attributes = bibSummary.attributes; + summary.holdCount = bibSummary.hold_count; + summary.holdingsSummary = bibSummary.copy_counts; + return summary; })); } - // A Metabib Summary is a BibRecordSummary with the lead record as - // its core bib record plus attributes (e.g. formats) from related - // records. - getMetabibSummary(metabibIds: number | number[], - orgId?: number, orgDepth?: number): Observable { - - const ids = [].concat(metabibIds); - - if (ids.length === 0) { - return from([]); - } - - return this.pcrud.search('mmr', {id: ids}, - {flesh: 1, flesh_fields: {mmr: ['source_maps']}}, - {anonymous: true} - ).pipe(mergeMap(mmr => this.compileMetabib(mmr, orgId, orgDepth))); - } - - // 'metabib' must have its "source_maps" field fleshed. - // Get bib summaries for all related bib records so we can - // extract data that must be appended to the master record summary. - compileMetabib(metabib: IdlObject, - orgId?: number, orgDepth?: number): Observable { - - // TODO: Create an API similar to the one that builds a combined - // mods blob for metarecords, except using display fields, etc. - // For now, this seems to get the job done. - - // Non-master records - const relatedBibIds = metabib.source_maps() - .map(m => m.source()) - .filter(id => id !== metabib.master_record()); + getMetabibSummaries(metabibIds: number[], + orgId?: number, isStaff?: boolean): Observable { - let observer; - const observable = new Observable(o => observer = o); + if (metabibIds.length === 0) { return from([]); } + if (!orgId) { orgId = this.org.root().id(); } - // NOTE: getBibSummary calls getHoldingsSummary against - // the bib record unnecessarily. It's called again below. - // Reconsider this approach (see also note above about API). - this.getBibSummary(metabib.master_record(), orgId, orgDepth) - .subscribe(summary => { - summary.metabibId = Number(metabib.id()); - summary.metabibRecords = - metabib.source_maps().map(m => Number(m.source())); + let method = 'open-ils.search.biblio.metabib.catalog_summary'; + if (isStaff) { method += '.staff'; } - let promise; - - if (relatedBibIds.length > 0) { - - // Grab data for MR bib summary augmentation - promise = this.pcrud.search('mraf', {id: relatedBibIds}) - .pipe(tap(attr => summary.record.mattrs().push(attr))) - .toPromise(); - } else { - - // Metarecord has only one constituent bib. - promise = Promise.resolve(); - } - - promise.then(() => { - - // Re-compile with augmented data - summary.compileRecordAttrs(); - - // Fetch holdings data for the metarecord - this.getHoldingsSummary(metabib.id(), orgId, orgDepth, true) - .then(holdingsSummary => { - summary.holdingsSummary = holdingsSummary; - observer.next(summary); - observer.complete(); - }); - }); - }); - - return observable; - } - - // Flesh the creator and editor fields. - // Handling this separately lets us pull from the cache and - // avoids the requirement that the main bib query use a staff - // (VIEW_USER) auth token. - fleshBibUsers(records: IdlObject[]): Promise { - - const search = []; - - records.forEach(rec => { - ['creator', 'editor'].forEach(field => { - const id = rec[field](); - if (Number.isInteger(id)) { - if (this.userCache[id]) { - rec[field](this.userCache[id]); - } else if (!search.includes(id)) { - search.push(id); - } - } - }); - }); - - if (search.length === 0) { - return Promise.resolve(); - } - - return this.pcrud.search('au', {id: search}) - .pipe(map(user => { - this.userCache[user.id()] = user; - records.forEach(rec => { - if (user.id() === rec.creator()) { - rec.creator(user); - } - if (user.id() === rec.editor()) { - rec.editor(user); - } - }); - })).toPromise(); - } - - getHoldingsSummary(recordId: number, - orgId: number, orgDepth: number, isMetarecord?: boolean): Promise { - - const holdingsSummary = []; - - return this.unapi.getAsXmlDocument({ - target: isMetarecord ? 'mmr' : 'bre', - id: recordId, - extras: '{holdings_xml}', - format: 'holdings_xml', - orgId: orgId, - depth: orgDepth - }).then(xmlDoc => { - - // namespace resolver - const resolver: any = (prefix: string): string => { - return NAMESPACE_MAPS[prefix] || null; - }; - - // Extract the holdings data from the unapi xml doc - const result = xmlDoc.evaluate(HOLDINGS_XPATH, - xmlDoc, resolver, XPathResult.ANY_TYPE, null); - - let node; - while (node = result.iterateNext()) { - const counts = {type : node.getAttribute('type')}; - ['depth', 'org_unit', 'transcendant', - 'available', 'count', 'unshadow'].forEach(field => { - counts[field] = Number(node.getAttribute(field)); - }); - holdingsSummary.push(counts); - } - - return holdingsSummary; - }); + return this.net.request('open-ils.search', method, orgId, metabibIds) + .pipe(map(metabibSummary => { + const summary = new BibRecordSummary(metabibSummary.record, orgId); + summary.net = this.net; // inject + summary.metabibId = Number(metabibSummary.metabib_id); + summary.metabibRecords = metabibSummary.metabib_records; + summary.display = metabibSummary.display; + summary.attributes = metabibSummary.attributes; + summary.holdCount = metabibSummary.hold_count; + summary.holdingsSummary = metabibSummary.copy_counts; + return summary; + })); } } diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts index c80d0b2683..805a0bb589 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts @@ -190,11 +190,11 @@ export class CatalogService { let observable: Observable; if (isMeta) { - observable = this.bibService.getMetabibSummary( - ctx.currentResultIds(), ctx.searchOrg.id(), depth); + observable = this.bibService.getMetabibSummaries( + ctx.currentResultIds(), ctx.searchOrg.id(), ctx.isStaff); } else { - observable = this.bibService.getBibSummary( - ctx.currentResultIds(), ctx.searchOrg.id(), depth); + observable = this.bibService.getBibSummaries( + ctx.currentResultIds(), ctx.searchOrg.id(), ctx.isStaff); } return observable.pipe(map(summary => { @@ -239,9 +239,9 @@ export class CatalogService { // them to bib IDs for highlighting. ids = ctx.currentResultIds(); if (ctx.termSearch.groupByMetarecord) { - ids = ids.map(mrId => - ctx.result.records.filter(r => mrId === r.metabibId)[0].id - ); + // The 4th slot in the result ID reports the master record + // for the metarecord in question. Sometimes it's null? + ids = ctx.result.ids.map(id => id[4]).filter(id => id !== null); } } diff --git a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts index dfbee692ef..10060e05af 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/vandelay/queued-record-matches.component.ts @@ -118,28 +118,24 @@ export class QueuedRecordMatchesComponent implements OnInit { }); const bibSummaries: {[id: number]: BibRecordSummary} = {}; - this.bib.getBibSummary(recIds).subscribe( + this.bib.getBibSummaries(recIds).subscribe( summary => bibSummaries[summary.id] = summary, err => {}, () => { - this.bib.fleshBibUsers( - Object.values(bibSummaries).map(sum => sum.record) - ).then(() => { - matches.forEach(match => { - const row = { - id: match.id(), - eg_record: match.eg_record(), - bre_quality: match.quality(), - vqbr_quality: this.queuedRecord.quality(), - match_score: match.match_score(), - bib_summary: bibSummaries[match.eg_record()] - }; - - observer.next(row); - }); - - observer.complete(); + matches.forEach(match => { + const row = { + id: match.id(), + eg_record: match.eg_record(), + bre_quality: match.quality(), + vqbr_quality: this.queuedRecord.quality(), + match_score: match.match_score(), + bib_summary: bibSummaries[match.eg_record()] + }; + + observer.next(row); }); + + observer.complete(); } ); }); diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/cnbrowse/results.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/cnbrowse/results.component.ts index 464f443a1f..9c0dab3dcf 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/cnbrowse/results.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/cnbrowse/results.component.ts @@ -119,9 +119,9 @@ export class CnBrowseResultsComponent implements OnInit, OnDestroy { }; const bres: IdlObject[] = []; - this.bib.getBibSummary( + this.bib.getBibSummaries( bibIds.filter(distinct), - this.searchContext.searchOrg.id(), depth + this.searchContext.searchOrg.id(), this.searchContext.isStaff ).subscribe( summary => { // Response order not guaranteed. Match the summary @@ -134,10 +134,6 @@ export class CnBrowseResultsComponent implements OnInit, OnDestroy { // Use _ since result is an 'acn' object. bibResults.forEach(r => r._bibSummary = summary); - }, - err => {}, - () => { - this.bib.fleshBibUsers(bres); } ); } diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts index e68e01791d..fc7ba54ea1 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts @@ -156,7 +156,6 @@ export class RecordComponent implements OnInit { .then(summary => { this.summary = this.staffCat.currentDetailRecordSummary = summary; - this.bib.fleshBibUsers([summary.record]); }); } diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html index 65209a0b4f..8d839e19b2 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html @@ -132,15 +132,13 @@
-
-
- - {{copyCount.available}} / {{copyCount.count}} items - -
-
- @ {{orgName(copyCount.org_unit)}} -
+
+ + {{copyCount.available}} / {{copyCount.count}} items + +
+
+ @ {{orgName(copyCount.org_unit)}}
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts index 8cb7f03c6c..664d36661e 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts @@ -43,7 +43,6 @@ export class ResultRecordComponent implements OnInit, OnDestroy { ngOnInit() { this.searchContext = this.staffCat.searchContext; - this.summary.getHoldCount(); this.isRecordSelected = this.basket.hasRecordId(this.summary.id); // Watch for basket changes caused by other components diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html index 41804cc62c..515a376dd9 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html @@ -65,12 +65,10 @@
-
-
-
- - -
+
+
+ +
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts index 50ed7914e4..edcb381044 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts @@ -100,39 +100,11 @@ export class ResultsComponent implements OnInit, OnDestroy { this.cat.search(this.searchContext) .then(ok => { this.cat.fetchFacets(this.searchContext); - this.cat.fetchBibSummaries(this.searchContext) - .then(ok2 => this.fleshSearchResults()); + this.cat.fetchBibSummaries(this.searchContext); }); } } - // Records file into place randomly as the server returns data. - // To reduce page display shuffling, avoid showing the list of - // records until the first few are ready to render. - shouldStartRendering(): boolean { - - if (this.searchHasResults()) { - const pageCount = this.searchContext.currentResultIds().length; - switch (pageCount) { - case 1: - return this.searchContext.result.records[0]; - default: - return this.searchContext.result.records[0] - && this.searchContext.result.records[1]; - } - } - - return false; - } - - fleshSearchResults(): void { - const records = this.searchContext.result.records; - if (!records || records.length === 0) { return; } - - // Flesh the creator / editor fields with the user object. - this.bib.fleshBibUsers(records.map(r => r.record)); - } - searchIsDone(): boolean { return this.searchContext.searchState === CatalogSearchState.COMPLETE; } diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html index 920b008074..d4ca900377 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html @@ -130,7 +130,7 @@
diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts index 39b8944d3a..84719fd3d5 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts @@ -66,7 +66,6 @@ export class BibSummaryComponent implements OnInit { this.bib.getBibSummary(this.recordId).toPromise() .then(summary => { summary.getBibCallNumber(); - this.bib.fleshBibUsers([summary.record]); this.summary = summary; }); } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm index 159ecd78b9..b07440e65a 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm @@ -13,9 +13,6 @@ use Encode; use OpenSRF::Utils::Logger qw/:logger/; - -use OpenSRF::Utils::JSON; - use Time::HiRes qw(time sleep); use OpenSRF::EX qw(:try); use Digest::MD5 qw(md5_hex); @@ -2744,5 +2741,158 @@ sub mk_copy_query { } +__PACKAGE__->register_method( + method => 'catalog_record_summary', + api_name => 'open-ils.search.biblio.record.catalog_summary', + stream => 1, + max_bundle_count => 1, + signature => { + desc => 'Stream of record data suitable for catalog display', + params => [ + {desc => 'Context org unit ID', type => 'number'}, + {desc => 'Array of Record IDs', type => 'array'} + ], + return => { + desc => q/ + Stream of record summary objects including id, record, + hold_count, copy_counts, display (metabib display + fields), attributes (metabib record attrs), plus + metabib_id and metabib_records for the metabib variant. + / + } + } +); +__PACKAGE__->register_method( + method => 'catalog_record_summary', + api_name => 'open-ils.search.biblio.record.catalog_summary.staff', + stream => 1, + max_bundle_count => 1, + signature => q/see open-ils.search.biblio.record.catalog_summary/ +); +__PACKAGE__->register_method( + method => 'catalog_record_summary', + api_name => 'open-ils.search.biblio.metabib.catalog_summary', + stream => 1, + max_bundle_count => 1, + signature => q/see open-ils.search.biblio.record.catalog_summary/ +); + +__PACKAGE__->register_method( + method => 'catalog_record_summary', + api_name => 'open-ils.search.biblio.metabib.catalog_summary.staff', + stream => 1, + max_bundle_count => 1, + signature => q/see open-ils.search.biblio.record.catalog_summary/ +); + + +sub catalog_record_summary { + my ($self, $client, $org_id, $record_ids) = @_; + my $e = new_editor(); + + my $is_meta = ($self->api_name =~ /metabib/); + my $is_staff = ($self->api_name =~ /staff/); + + my $holds_method = $is_meta ? + 'open-ils.circ.mmr.holds.count' : + 'open-ils.circ.bre.holds.count'; + + my $copy_method = $is_meta ? + 'open-ils.search.biblio.metarecord.copy_count': + 'open-ils.search.biblio.record.copy_count'; + + $copy_method .= '.staff' if $is_staff; + + $copy_method = $self->method_lookup($copy_method); # local method + + for my $rec_id (@$record_ids) { + + my $response = $is_meta ? + get_one_metarecord_summary($e, $rec_id) : + get_one_record_summary($e, $rec_id); + + ($response->{copy_counts}) = $copy_method->run($org_id, $rec_id); + + $response->{hold_count} = + $U->simplereq('open-ils.circ', $holds_method, $rec_id); + + $client->respond($response); + } + + return undef; +} + +# Start with a bib summary and augment the data with additional +# metarecord content. +sub get_one_metarecord_summary { + my ($e, $rec_id) = @_; + + my $meta = $e->retrieve_metabib_metarecord($rec_id) or return {}; + my $maps = $e->search_metabib_metarecord_source_map({metarecord => $rec_id}); + + my $bre_id = $meta->master_record; + + my $response = get_one_record_summary($e, $bre_id); + + $response->{metabib_id} = $rec_id; + $response->{metabib_records} = [map {$_->source} @$maps]; + + my @other_bibs = map {$_->source} grep {$_->source != $bre_id} @$maps; + + # Augment the record attributes with those of all of the records + # linked to this metarecord. + if (@other_bibs) { + my $attrs = $e->search_metabib_record_attr_flat({id => \@other_bibs}); + + my $attributes = $response->{attributes}; + + for my $attr (@$attrs) { + $attributes->{$attr->attr} = [] unless $attributes->{$attr->attr}; + push(@{$attributes->{$attr->attr}}, $attr->value) # avoid dupes + unless grep {$_ eq $attr->value} @{$attributes->{$attr->attr}}; + } + } + + return $response; +} + +sub get_one_record_summary { + my ($e, $rec_id) = @_; + + my $bre = $e->retrieve_biblio_record_entry([$rec_id, { + flesh => 1, + flesh_fields => { + bre => [qw/compressed_display_entries mattrs creator editor/] + } + }]) or return {}; + + # Compressed display fields are pachaged as JSON + my $display = {}; + $display->{$_->name} = OpenSRF::Utils::JSON->JSON2perl($_->value) + foreach @{$bre->compressed_display_entries}; + + # Create an object of 'mraf' attributes. + # Any attribute can be multi so dedupe and array-ify all of them. + my $attributes = {}; + for my $attr (@{$bre->mattrs}) { + $attributes->{$attr->attr} = {} unless $attributes->{$attr->attr}; + $attributes->{$attr->attr}->{$attr->value} = 1; # avoid dupes + } + $attributes->{$_} = [keys %{$attributes->{$_}}] for keys %$attributes; + + # clear bulk + $bre->clear_marc; + $bre->clear_mattrs; + $bre->clear_compressed_display_entries; + + return { + id => $rec_id, + record => $bre, + display => $display, + attributes => $attributes + }; +} + + 1; -- 2.43.2