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;
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<number> {
-
- 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<string> {
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<BibRecordSummary> {
+ 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<BibRecordSummary> {
+ getBibSummaries(bibIds: number[],
+ orgId?: number, isStaff?: boolean): Observable<BibRecordSummary> {
- 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<BibRecordSummary> {
-
- 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<BibRecordSummary> {
-
- // 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<BibRecordSummary> {
- let observer;
- const observable = new Observable<BibRecordSummary>(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<void> {
-
- 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<any> {
-
- 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;
+ }));
}
}
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);
}
+__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;