diff --git a/app.js b/app.js index a279b95a..4d5c09e2 100644 --- a/app.js +++ b/app.js @@ -3,6 +3,7 @@ const express = require('express') const esClient = require('./lib/elasticsearch/client') const loadConfig = require('./lib/load-config') const { preflightCheck } = require('./lib/preflight_check') +const { loadNyplCoreData } = require('./lib/load_nypl_core') const swaggerDocs = require('./swagger.v1.1.x.json') @@ -20,7 +21,7 @@ app.set('trust proxy', 'loopback') app.init = async () => { await loadConfig.loadConfig() - + await loadNyplCoreData() preflightCheck() // Load logger after running above to ensure we respect LOG_LEVEL if set diff --git a/config/production.env b/config/production.env index 5374f99a..31427a5e 100644 --- a/config/production.env +++ b/config/production.env @@ -10,7 +10,7 @@ NYPL_OAUTH_URL=https://isso.nypl.org/ ENCRYPTED_NYPL_OAUTH_ID=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAGswaQYJKoZIhvcNAQcGoFwwWgIBADBVBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDMLKVUQA58B6vprNcAIBEIAoaz0lI9EL2M9NyTuEwT8JDmPBt6aXfMiFs027DEuwsCN0wS0qWeFL1g== ENCRYPTED_NYPL_OAUTH_SECRET=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAIcwgYQGCSqGSIb3DQEHBqB3MHUCAQAwcAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAyWz91LOP2YP5fg0q0CARCAQ9inO9SV1M8R0Pkkx84r7UdwlU1FxfXvIjk/z6Qs81KBAVELhby2iD5LawQyDrR9tjhuMbotS6QnydwwMR/p8+qJXHI= -NYPL_CORE_VERSION=v2.22 +NYPL_CORE_VERSION=v2.23 LOG_LEVEL=info FEATURES=on-site-edd diff --git a/config/qa.env b/config/qa.env index 186c4579..f27a0361 100644 --- a/config/qa.env +++ b/config/qa.env @@ -10,7 +10,7 @@ NYPL_OAUTH_URL=https://isso.nypl.org/ ENCRYPTED_NYPL_OAUTH_ID=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAGswaQYJKoZIhvcNAQcGoFwwWgIBADBVBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDMLKVUQA58B6vprNcAIBEIAoaz0lI9EL2M9NyTuEwT8JDmPBt6aXfMiFs027DEuwsCN0wS0qWeFL1g== ENCRYPTED_NYPL_OAUTH_SECRET=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAIcwgYQGCSqGSIb3DQEHBqB3MHUCAQAwcAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAyWz91LOP2YP5fg0q0CARCAQ9inO9SV1M8R0Pkkx84r7UdwlU1FxfXvIjk/z6Qs81KBAVELhby2iD5LawQyDrR9tjhuMbotS6QnydwwMR/p8+qJXHI= -NYPL_CORE_VERSION=v2.22 +NYPL_CORE_VERSION=v2.24 LOG_LEVEL=info FEATURES=on-site-edd diff --git a/config/test.env b/config/test.env index f3f39d0b..9ab73b92 100644 --- a/config/test.env +++ b/config/test.env @@ -9,7 +9,7 @@ NYPL_OAUTH_URL=http://oauth.example.com ENCRYPTED_NYPL_OAUTH_ID=encrypted-nypl-oauth-id ENCRYPTED_NYPL_OAUTH_SECRET=encrypted-nypl-oauth-id -NYPL_CORE_VERSION=v2.21 +NYPL_CORE_VERSION=v2.23 LOG_LEVEL=error FEATURES=on-site-edd diff --git a/lib/available_delivery_location_types.js b/lib/available_delivery_location_types.js index da9c4c3f..43183395 100644 --- a/lib/available_delivery_location_types.js +++ b/lib/available_delivery_location_types.js @@ -1,18 +1,18 @@ const logger = require('./logger') const { makeNyplDataApiClient } = require('./data-api-client') +const nyplCore = require('./load_nypl_core') class AvailableDeliveryLocationTypes { static getScholarRoomByPatronId (patronID) { // If patronID is falsy (i.e. patron is not logged in) they're just a Rearcher: if (!patronID) return Promise.resolve(['Research']) - const patronTypeMapping = require('@nypl/nypl-core-objects')('by-patron-type') return this._getPatronTypeOf(patronID) .then((patronType) => { - if (this._isUnfamiliarPatronType(patronTypeMapping, patronType)) { + if (this._isUnfamiliarPatronType(patronType)) { return } - const patronTypeData = patronTypeMapping[patronType] + const patronTypeData = nyplCore.patronTypes()[patronType] return patronTypeData.scholarRoom && patronTypeData.scholarRoom.code }) } @@ -38,8 +38,8 @@ class AvailableDeliveryLocationTypes { }) } - static _isUnfamiliarPatronType (patronTypeMapping, patronType) { - if (!patronTypeMapping[patronType]) { + static _isUnfamiliarPatronType (patronType) { + if (!nyplCore.patronTypes()[patronType]) { logger.info(`Found the Patron Type: ${patronType} is not recognizable.`) return true } else { @@ -48,8 +48,4 @@ class AvailableDeliveryLocationTypes { } } -const patronTypeMapping = require('@nypl/nypl-core-objects')('by-patron-type') - -AvailableDeliveryLocationTypes.patronTypeMapping = patronTypeMapping - module.exports = AvailableDeliveryLocationTypes diff --git a/lib/delivery-locations-resolver.js b/lib/delivery-locations-resolver.js index 86ac983d..eafc84e5 100644 --- a/lib/delivery-locations-resolver.js +++ b/lib/delivery-locations-resolver.js @@ -1,18 +1,18 @@ const { itemHasRecapHoldingLocation, barcodeFromItem } = require('./util') const scsbClient = require('./scsb-client') -const recapCustomerCodes = require('@nypl/nypl-core-objects')('by-recap-customer-code') -const sierraLocations = require('@nypl/nypl-core-objects')('by-sierra-location') +const nyplCore = require('./load_nypl_core') + const logger = require('./logger') const onsiteEddCriteria = require('../data/onsite-edd-criteria.json') const { isItemNyplOwned } = require('./ownership_determination') class DeliveryLocationsResolver { static nyplCoreLocation (locationCode) { - return sierraLocations[locationCode] + return nyplCore.sierraLocations()[locationCode] } static requestableBasedOnHoldingLocation (item) { - const locationCode = this.extractLocationCode(item) + const locationCode = DeliveryLocationsResolver.extractLocationCode(item) if (!DeliveryLocationsResolver.nyplCoreLocation(locationCode)) { logger.warn(`DeliveryLocationsResolver: Unrecognized holdingLocation for ${item.uri}: ${locationCode}`) @@ -50,21 +50,15 @@ class DeliveryLocationsResolver { // Fetch Sierra delivery locations by recap code static deliveryLocationsByRecapCustomerCode (customerCode) { - if (recapCustomerCodes[customerCode] && recapCustomerCodes[customerCode].sierraDeliveryLocations) { - return recapCustomerCodes[customerCode].sierraDeliveryLocations + if (nyplCore.recapCustomerCodes()[customerCode] && nyplCore.recapCustomerCodes()[customerCode].sierraDeliveryLocations) { + return nyplCore.recapCustomerCodes()[customerCode].sierraDeliveryLocations } } // Fetch Sierra delivery locations by m2 customer code. Returns undefined if the m2 customer code is not requestable: static deliveryLocationsByM2CustomerCode (customerCode) { - let m2CustomerCodes - try { - m2CustomerCodes = require('@nypl/nypl-core-objects')('by-m2-customer-code') - } catch (e) { - - } - if (m2CustomerCodes && m2CustomerCodes[customerCode] && m2CustomerCodes[customerCode].sierraDeliveryLocations) { - const { sierraDeliveryLocations, requestable } = m2CustomerCodes[customerCode] + if (nyplCore.m2CustomerCodes()?.[customerCode]?.sierraDeliveryLocations) { + const { sierraDeliveryLocations, requestable } = nyplCore.m2CustomerCodes()[customerCode] if (requestable) { return sierraDeliveryLocations } else return undefined @@ -73,7 +67,7 @@ class DeliveryLocationsResolver { // Determine eddRequestable by recap customer code: static __eddRequestableByCustomerCode (customerCode) { - if (recapCustomerCodes[customerCode]) return Boolean(recapCustomerCodes[customerCode].eddRequestable) + if (nyplCore.recapCustomerCodes()[customerCode]) return Boolean(nyplCore.recapCustomerCodes()[customerCode].eddRequestable) } // Determine eddRequestable by on-site EDD requestability criteria (presumed on-site): @@ -172,7 +166,7 @@ class DeliveryLocationsResolver { return { id: `loc:${location.code}`, label: location.label, - sortPosition: this.sortPosition(location) + sortPosition: DeliveryLocationsResolver.sortPosition(location) } }) // Either way, sort deliveryLocation entries by name: @@ -197,14 +191,14 @@ class DeliveryLocationsResolver { } static attachRecapDeliveryInfo (item) { - const info = this.getRecapDeliveryInfo(item) + const info = DeliveryLocationsResolver.getRecapDeliveryInfo(item) item.eddRequestable = info.eddRequestable item.deliveryLocation = info.deliveryLocation return item } static attachOnsiteDeliveryInfo (item) { - const info = this.getOnsiteDeliveryInfo(item) + const info = DeliveryLocationsResolver.getOnsiteDeliveryInfo(item) item.eddRequestable = info.eddRequestable item.deliveryLocation = info.deliveryLocation return item @@ -218,15 +212,15 @@ class DeliveryLocationsResolver { const hasRecapCustomerCode = item.recapCustomerCode && item.recapCustomerCode[0] const nyplItem = isItemNyplOwned(item) if (!hasRecapCustomerCode) { - const requestableBasedOnHoldingLocation = nyplItem ? this.requestableBasedOnHoldingLocation(item) : true + const requestableBasedOnHoldingLocation = nyplItem ? DeliveryLocationsResolver.requestableBasedOnHoldingLocation(item) : true // the length of the list of delivery locations is checked later to determine physical requestability // In case of an offsite item with no recap customer code, we want this to be based on holding location // so we put a placeholder '' in case it is requestable based on holding location deliveryLocation = requestableBasedOnHoldingLocation ? [''] : [] eddRequestable = requestableBasedOnHoldingLocation - } else if (!nyplItem || this.requestableBasedOnHoldingLocation(item)) { - deliveryLocation = this.deliveryLocationsByRecapCustomerCode(item.recapCustomerCode[0]) - eddRequestable = this.__eddRequestableByCustomerCode(item.recapCustomerCode[0]) + } else if (!nyplItem || DeliveryLocationsResolver.requestableBasedOnHoldingLocation(item)) { + deliveryLocation = DeliveryLocationsResolver.deliveryLocationsByRecapCustomerCode(item.recapCustomerCode[0]) + eddRequestable = DeliveryLocationsResolver.__eddRequestableByCustomerCode(item.recapCustomerCode[0]) } else { deliveryLocation = [] eddRequestable = false @@ -239,7 +233,7 @@ class DeliveryLocationsResolver { eddRequestable: false, deliveryLocation: [] } - const holdingLocationCode = this.extractLocationCode(item) + const holdingLocationCode = DeliveryLocationsResolver.extractLocationCode(item) const sierraData = DeliveryLocationsResolver.nyplCoreLocation(holdingLocationCode) if (!sierraData) { // This case is mainly to satisfy a test which wants eddRequestable = false @@ -249,15 +243,15 @@ class DeliveryLocationsResolver { } // if nypl core says it's unrequestable, it can still be eddRequestable, // but its definitely not phys requestable. - deliveryInfo.eddRequestable = this.eddRequestableByOnSiteCriteria(item) - if (!this.requestableBasedOnHoldingLocation(item)) { + deliveryInfo.eddRequestable = DeliveryLocationsResolver.eddRequestableByOnSiteCriteria(item) + if (!DeliveryLocationsResolver.requestableBasedOnHoldingLocation(item)) { return deliveryInfo } // if nypl-core reports that a holding location's delivery locations // should be found by M2 code, but only if the item has an M2 customer code const deliverableToResolution = sierraData.deliverableToResolution if (deliverableToResolution === 'm2-customer-code' && item.m2CustomerCode && item.m2CustomerCode[0]) { - deliveryInfo.deliveryLocation = this.deliveryLocationsByM2CustomerCode(item.m2CustomerCode[0]) + deliveryInfo.deliveryLocation = DeliveryLocationsResolver.deliveryLocationsByM2CustomerCode(item.m2CustomerCode[0]) } // if no value, default to sierra location lookup if (!deliverableToResolution) { @@ -281,15 +275,15 @@ class DeliveryLocationsResolver { } /** - * Given an array of items (ES hits), returns the same items with `eddRequestable` & `deliveryLocations` + * Given an array of items (ES hits), returns the same items with `eddRequestable` & `deliveryLocations`. We verify all recap customer codes because our indexed data may be stale. * * @return Promise> A Promise that resolves and array of items, modified to include `eddRequestable` & `deliveryLocations` */ static attachDeliveryLocationsAndEddRequestability (items, scholarRoom) { // Extract ReCAP barcodes from items: - const recapBarcodes = this.extractRecapBarcodes(items) + const recapBarcodes = DeliveryLocationsResolver.extractRecapBarcodes(items) // Get a map from barcodes to ReCAP customercodes: - return this.__recapCustomerCodesByBarcodes(recapBarcodes) + return DeliveryLocationsResolver.__recapCustomerCodesByBarcodes(recapBarcodes) .then((barcodeToRecapCustomerCode) => { // Now map over items to affix deliveryLocations: return items.map((item) => { @@ -298,15 +292,15 @@ class DeliveryLocationsResolver { item.recapCustomerCode = [barcodeToRecapCustomerCode[barcode]] // If recap has a customer code for this barcode, map it by recap cust code: if (item.recapCustomerCode[0]) { - item = this.attachRecapDeliveryInfo(item) + item = DeliveryLocationsResolver.attachRecapDeliveryInfo(item) // Otherwise, it's an onsite item } else { - item = this.attachOnsiteDeliveryInfo(item) + item = DeliveryLocationsResolver.attachOnsiteDeliveryInfo(item) } // Establish default for Electronic Document Delivery flag: item.eddRequestable = !!item.eddRequestable - const filteredDeliveryLocationsWithScholarRoom = this.filterLocations(item.deliveryLocation, scholarRoom) - item.deliveryLocation = this.formatLocations(filteredDeliveryLocationsWithScholarRoom) + const filteredDeliveryLocationsWithScholarRoom = DeliveryLocationsResolver.filterLocations(item.deliveryLocation, scholarRoom) + item.deliveryLocation = DeliveryLocationsResolver.formatLocations(filteredDeliveryLocationsWithScholarRoom) return item }) }) diff --git a/lib/jsonld_serializers.js b/lib/jsonld_serializers.js index 73ab8c85..026d8f1e 100644 --- a/lib/jsonld_serializers.js +++ b/lib/jsonld_serializers.js @@ -1,7 +1,6 @@ 'use strict' -const locations = require('@nypl/nypl-core-objects')('by-sierra-location') -const recordTypes = require('@nypl/nypl-core-objects')('by-record-types') +const nyplCore = require('./load_nypl_core') const NyplSourceMapper = require('research-catalog-indexer/lib/utils/nypl-source-mapper') const util = require('./util.js') @@ -287,7 +286,7 @@ class ResourceSerializer extends JsonLdItemSerializer { } ResourceSerializer.getFormattedRecordType = function (recordTypeId) { - const prefLabel = recordTypes[recordTypeId]?.label + const prefLabel = nyplCore.recordTypes()[recordTypeId]?.label if (!prefLabel) return null return { '@id': recordTypeId, @@ -499,10 +498,10 @@ class AggregationSerializer extends JsonLdItemSerializer { v.label = p[1] } else if (field === 'buildingLocation') { // Build buildingLocation agg labels from nypl-core: - v.label = locations[v.value]?.label + v.label = nyplCore.sierraLocations()[v.value]?.label } else if (field === 'recordType') { // Build recordType agg labels from nypl-core: - v.label = recordTypes[v.value]?.label + v.label = nyplCore.recordTypes()[v.value]?.label // Unknown recordType? Remove it: if (!v.label) return null } else { diff --git a/lib/load_nypl_core.js b/lib/load_nypl_core.js new file mode 100644 index 00000000..1206bde4 --- /dev/null +++ b/lib/load_nypl_core.js @@ -0,0 +1,25 @@ +const _data = {} +const nyplCoreObjects = require('@nypl/nypl-core-objects') + +const loadNyplCoreData = () => { + const vocabularies = { + sierraLocations: 'by-sierra-location', + recordTypes: 'by-record-types', + recapCustomerCodes: 'by-recap-customer-code', + m2CustomerCodes: 'by-m2-customer-code', + patronTypes: 'by-patron-type' + } + return Promise.all(Object.keys(vocabularies).map(async (vocab) => { + const nyplCoreValues = await nyplCoreObjects(vocabularies[vocab]) + _data[vocab] = nyplCoreValues + })) +} + +module.exports = { + loadNyplCoreData, + patronTypes: () => _data.patronTypes || {}, + sierraLocations: () => _data.sierraLocations || {}, + recapCustomerCodes: () => _data.recapCustomerCodes || {}, + recordTypes: () => _data.recordTypes || {}, + m2CustomerCodes: () => _data.m2CustomerCodes || {} +} diff --git a/lib/location_label_updater.js b/lib/location_label_updater.js index c23f787a..6a07e216 100644 --- a/lib/location_label_updater.js +++ b/lib/location_label_updater.js @@ -1,4 +1,4 @@ -const sierraLocations = require('@nypl/nypl-core-objects')('by-sierra-location') +const nyplCore = require('./load_nypl_core') class LocationLabelUpdater { constructor (responseReceived) { @@ -10,20 +10,22 @@ class LocationLabelUpdater { const resp = this.elasticSearchResponse const updatedHits = resp.hits.hits.map((bib) => { // Update locations for items: - ; (bib._source.items || []).forEach((item) => { + const items = bib._source.items || [] + items.forEach((item) => { if (item.holdingLocation && item.holdingLocation.length > 0) { item.holdingLocation = item.holdingLocation.map((loc) => { - const nyplCoreEntry = sierraLocations[loc.id.replace(/^loc:/, '')] + const nyplCoreEntry = nyplCore.sierraLocations()[loc.id.replace(/^loc:/, '')] if (nyplCoreEntry) loc.label = nyplCoreEntry.label return loc }) } }) // Update locations for holdings: - ; (bib._source.holdings || []).forEach((holding) => { + const holdings = bib._source.holdings || [] + holdings.forEach((holding) => { if (holding.location && holding.location.length > 0) { holding.location = holding.location.map((loc) => { - const nyplCoreEntry = sierraLocations[loc.code.replace(/^loc:/, '')] + const nyplCoreEntry = nyplCore.sierraLocations()[loc.code.replace(/^loc:/, '')] if (nyplCoreEntry) loc.label = nyplCoreEntry.label return loc }) diff --git a/lib/requestability_resolver.js b/lib/requestability_resolver.js index 0b192d6c..cb3b8b82 100644 --- a/lib/requestability_resolver.js +++ b/lib/requestability_resolver.js @@ -2,39 +2,42 @@ const DeliveryLocationsResolver = require('./delivery-locations-resolver') const { isItemNyplOwned } = require('./ownership_determination') const { isInRecap } = require('./util') const logger = require('./logger') + class RequestabilityResolver { static fixItemRequestability (elasticSearchResponse) { elasticSearchResponse.hits.hits .forEach((hit) => { + const parentBibHasFindingAid = !!hit._source.supplementaryContent?.find((el) => el.label.toLowerCase() === 'finding aid') hit._source.items = hit._source.items.map((item) => { if (item.electronicLocator) return item let deliveryInfo const itemIsInRecap = isInRecap(item) let physRequestableCriteria - const hasRecapCustomerCode = item.recapCustomerCode && item.recapCustomerCode[0] + const hasRecapCustomerCode = item.recapCustomerCode?.[0] if (itemIsInRecap) { // recap items missing codes should default to true for phys and edd // requestable, unless it has a non-requestable holding location deliveryInfo = DeliveryLocationsResolver.getRecapDeliveryInfo(item) physRequestableCriteria = hasRecapCustomerCode - ? `${(deliveryInfo.deliveryLocation && - deliveryInfo.deliveryLocation.length) || 0} delivery locations.` + ? `${(deliveryInfo.deliveryLocation?.length) || 0} delivery locations.` : 'Missing customer code' } else if (!itemIsInRecap) { deliveryInfo = DeliveryLocationsResolver.getOnsiteDeliveryInfo(item) - physRequestableCriteria = `${(deliveryInfo.deliveryLocation && - deliveryInfo.deliveryLocation.length) || 0} delivery locations.` + physRequestableCriteria = `${(deliveryInfo.deliveryLocation?.length) || 0} delivery locations.` } - item.eddRequestable = !!deliveryInfo.eddRequestable - item.physRequestable = !!(deliveryInfo.deliveryLocation && - deliveryInfo.deliveryLocation.length) - item.specRequestable = !!item.aeonUrl + item.specRequestable = this.buildSpecRequestable(item, parentBibHasFindingAid) + item.physRequestable = !!deliveryInfo.deliveryLocation?.length + item.eddRequestable = !!deliveryInfo.eddRequestable && !item.specRequestable // items without barcodes should not be requestable const hasBarcode = (item.identifier || []).some((identifier) => /^(urn|bf):[bB]arcode:\w+/.test(identifier)) if (isItemNyplOwned(item) && !hasBarcode) { physRequestableCriteria = 'NYPL item missing barcode' item.physRequestable = false } + if (item.specRequestable && item.physRequestable) { + item.physRequestable = false + physRequestableCriteria = 'specRequestable overrides physRequestable location' + } logger.debug(`item ${item.uri}: `, { physRequestable: item.physRequestable, physRequestableCriteria }) item.requestable = [item.eddRequestable || item.physRequestable || item.specRequestable] return item @@ -42,6 +45,13 @@ class RequestabilityResolver { }) return elasticSearchResponse } + + static buildSpecRequestable (item, parentBibHasFindingAid) { + const holdingLocation = DeliveryLocationsResolver.extractLocationCode(item) + const nyplCoreLocation = DeliveryLocationsResolver.nyplCoreLocation(holdingLocation) + const isSpecialCollectionsOnlyAccessType = !!(nyplCoreLocation?.collectionAccessType === 'special') + return !!item.aeonUrl || parentBibHasFindingAid || isSpecialCollectionsOnlyAccessType + } } module.exports = RequestabilityResolver diff --git a/package-lock.json b/package-lock.json index 95ca6716..d91e7e20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@aws-sdk/client-kms": "^3.533.0", "@elastic/elasticsearch": "~8.12.0", - "@nypl/nypl-core-objects": "2.3.2", + "@nypl/nypl-core-objects": "3.0.3", "@nypl/nypl-data-api-client": "^2.0.0", "@nypl/scsb-rest-client": "3.0.0", "dotenv": "^16.4.5", @@ -4922,87 +4922,44 @@ } }, "node_modules/@nypl/nypl-core-objects": { - "version": "2.3.2", - "license": "MIT", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@nypl/nypl-core-objects/-/nypl-core-objects-3.0.3.tgz", + "integrity": "sha512-tnqqCRsaydRQACaBf5IFMiKVEMKwevWVsPrwnbOsMkGqmIrM023d4T+NILfeOEYD74P8R8StwYCThuEt7G+8rw==", "dependencies": { + "axios": "^1.6.8", "csv": "^5.3.2", "csv-stringify": "^5.6.0", - "just-flatten": "^1.0.0", - "sync-request": "^6.1.0" + "just-flatten": "^1.0.0" } }, - "node_modules/@nypl/nypl-core-objects/node_modules/caseless": { - "version": "0.12.0", - "license": "Apache-2.0" + "node_modules/@nypl/nypl-core-objects/node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/@nypl/nypl-core-objects/node_modules/csv-stringify": { "version": "5.6.5", "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", "integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==" }, - "node_modules/@nypl/nypl-core-objects/node_modules/http-basic": { - "version": "8.1.3", - "license": "MIT", - "dependencies": { - "caseless": "^0.12.0", - "concat-stream": "^1.6.2", - "http-response-object": "^3.0.1", - "parse-cache-control": "^1.0.1" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@nypl/nypl-core-objects/node_modules/http-response-object": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "@types/node": "^10.0.3" - } - }, - "node_modules/@nypl/nypl-core-objects/node_modules/promise": { - "version": "8.3.0", - "license": "MIT", - "dependencies": { - "asap": "~2.0.6" - } - }, - "node_modules/@nypl/nypl-core-objects/node_modules/sync-request": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "http-response-object": "^3.0.1", - "sync-rpc": "^1.2.1", - "then-request": "^6.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nypl/nypl-core-objects/node_modules/then-request": { - "version": "6.0.2", - "license": "MIT", + "node_modules/@nypl/nypl-core-objects/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dependencies": { - "@types/concat-stream": "^1.6.0", - "@types/form-data": "0.0.33", - "@types/node": "^8.0.0", - "@types/qs": "^6.2.31", - "caseless": "~0.12.0", - "concat-stream": "^1.6.0", - "form-data": "^2.2.0", - "http-basic": "^8.1.1", - "http-response-object": "^3.0.1", - "promise": "^8.0.0", - "qs": "^6.4.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=6.0.0" + "node": ">= 6" } }, - "node_modules/@nypl/nypl-core-objects/node_modules/then-request/node_modules/@types/node": { - "version": "8.10.66", - "license": "MIT" - }, "node_modules/@nypl/nypl-data-api-client": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@nypl/nypl-data-api-client/-/nypl-data-api-client-2.0.0.tgz", @@ -5022,46 +4979,6 @@ "nypl-data-api": "bin/nypl-data-api.js" } }, - "node_modules/@nypl/nypl-data-api-client/node_modules/@nypl/nypl-core-objects": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nypl/nypl-core-objects/-/nypl-core-objects-3.0.0.tgz", - "integrity": "sha512-chFXev9uMpL0mU+y5BTvC2/4mOwhXNicTVfIqnasAqNWbOClEHhS/fIPod/q1TjdkD5jqbTlMVxsW/JjE3UjFg==", - "dependencies": { - "axios": "^1.6.8", - "csv": "^5.3.2", - "csv-stringify": "^5.6.0", - "just-flatten": "^1.0.0" - } - }, - "node_modules/@nypl/nypl-data-api-client/node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/@nypl/nypl-data-api-client/node_modules/csv-stringify": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", - "integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==" - }, - "node_modules/@nypl/nypl-data-api-client/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "license": "BSD-2-Clause", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@nypl/nypl-streams-client": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@nypl/nypl-streams-client/-/nypl-streams-client-2.0.0.tgz", @@ -8293,7 +8210,9 @@ }, "node_modules/form-data": { "version": "2.3.3", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -11317,6 +11236,17 @@ "node": ">=10" } }, + "node_modules/research-catalog-indexer/node_modules/@nypl/nypl-core-objects": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@nypl/nypl-core-objects/-/nypl-core-objects-2.3.2.tgz", + "integrity": "sha512-5IRW6y/kv6Oh82nnLaSDMjJj3rKSG+YhVum5LsY/2Heoq3Jve+p31Et9DvymgqaZNldSADRTR2GPwdfovOiCVg==", + "dependencies": { + "csv": "^5.3.2", + "csv-stringify": "^5.6.0", + "just-flatten": "^1.0.0", + "sync-request": "^6.1.0" + } + }, "node_modules/research-catalog-indexer/node_modules/@nypl/scsb-rest-client": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@nypl/scsb-rest-client/-/scsb-rest-client-2.0.0.tgz", @@ -11350,6 +11280,11 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, + "node_modules/research-catalog-indexer/node_modules/csv-stringify": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", + "integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==" + }, "node_modules/research-catalog-indexer/node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", diff --git a/package.json b/package.json index e83f91bf..719ba346 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "dependencies": { "@aws-sdk/client-kms": "^3.533.0", "@elastic/elasticsearch": "~8.12.0", - "@nypl/nypl-core-objects": "2.3.2", + "@nypl/nypl-core-objects": "3.0.3", "@nypl/nypl-data-api-client": "^2.0.0", "@nypl/scsb-rest-client": "3.0.0", "dotenv": "^16.4.5", @@ -65,4 +65,4 @@ "type": "git", "url": "https://github.com/NYPL/discovery-api.git" } -} +} \ No newline at end of file diff --git a/test/available_delivery_location_types.test.js b/test/available_delivery_location_types.test.js index 00787530..89c1f349 100644 --- a/test/available_delivery_location_types.test.js +++ b/test/available_delivery_location_types.test.js @@ -1,8 +1,7 @@ const fixtures = require('./fixtures') +const AvailableDeliveryLocationTypes = require('../lib/available_delivery_location_types.js') describe('AvailableDeliveryLocationTypes', function () { - const AvailableDeliveryLocationTypes = require('../lib/available_delivery_location_types.js') - before(function () { // Reroute these (and only these) api paths to local fixtures: fixtures.enableDataApiFixtures({ diff --git a/test/fixtures/specRequestable/findingAid-es-response.js b/test/fixtures/specRequestable/findingAid-es-response.js new file mode 100644 index 00000000..7de9c342 --- /dev/null +++ b/test/fixtures/specRequestable/findingAid-es-response.js @@ -0,0 +1,152 @@ +module.exports = () => { + return { + _shards: { + failed: 0, + successful: 1, + total: 1 + }, + took: 1, + hits: { + total: 1, + max_score: 1.3862944, + hits: [ + { + _id: 'b10980129', + _source: { + supplementaryContent: [{ + label: 'Finding aid', + url: 'spaghetti.com' + }], + items: [ + { + holdingLocation: [ + { + label: 'OFFSITE - Request in Advance', + id: 'loc:rc2ma' + } + ], + status_packed: [ + 'status:na||Not Available' + ], + owner: [ + { + id: 'orgs:1000', + label: 'Stephen A. Schwarzman Building' + } + ], + deliveryLocation: [ + { + id: 'loc:mala', + label: 'SASB - Allen Scholar Room' + } + ], + deliveryLocation_packed: [ + 'loc:mala||SASB - Allen Scholar Room' + ], + uri: 'i10283664', + accessMessage_packed: [ + 'accessMessage:2||ADV REQUEST' + ], + accessMessage: [ + { + id: 'accessMessage:2', + label: 'ADV REQUEST' + } + ], + status: [ + { + id: 'status:na', + label: 'Not available' + } + ], + owner_packed: [ + 'orgs:1000||Stephen A. Schwarzman Building' + ], + requestable: [ + false + ], + identifier: [ + 'urn:barcode:1000546836' + ], + holdingLocation_packed: [ + 'loc:rc2ma||OFFSITE - Request in Advance' + ], + shelfMark: [ + '*OFC 90-2649' + ], + suppressed: [ + false + ] + }, + { + holdingLocation: [ + { + label: 'OFFSITE - Request in Advance', + id: 'loc:rc2ma' + } + ], + status_packed: [ + 'status:a||Available' + ], + owner: [ + { + id: 'orgs:1000', + label: 'Stephen A. Schwarzman Building' + } + ], + deliveryLocation: [ + { + id: 'loc:mala', + label: 'SASB - Allen Scholar Room' + } + ], + deliveryLocation_packed: [ + 'loc:mala||SASB - Allen Scholar Room' + ], + uri: 'i102836649', + accessMessage_packed: [ + 'accessMessage:2||ADV REQUEST' + ], + accessMessage: [ + { + id: 'accessMessage:2', + label: 'ADV REQUEST' + } + ], + status: [ + { + id: 'status:a', + label: 'Available' + } + ], + owner_packed: [ + 'orgs:1000||Stephen A. Schwarzman Building' + ], + requestable: [ + false + ], + identifier: [ + 'urn:barcode:10005468369' + ], + holdingLocation_packed: [ + 'loc:rc2ma||OFFSITE - Request in Advance' + ], + shelfMark: [ + '*OFC 90-2649 2' + ], + suppressed: [ + false + ] + } + ] + + }, + _type: 'resource', + _index: 'resources-2017-06-13', + _score: 154.93451 + } + ] + }, + timed_out: false + } +} diff --git a/test/fixtures/specRequestable/phys-requestable-override.js b/test/fixtures/specRequestable/phys-requestable-override.js new file mode 100644 index 00000000..6c022618 --- /dev/null +++ b/test/fixtures/specRequestable/phys-requestable-override.js @@ -0,0 +1,297 @@ +module.exports = { + _shards: { + failed: 0, + successful: 1, + total: 1 + }, + took: 1, + hits: { + total: 1, + max_score: 1.3862944, + hits: [ + { + _id: 'b10980129', + _source: { + numItems: [ + 4 + ], + createdString: [ + '1989' + ], + issuance: [ + { + label: 'monograph/item', + id: 'urn:biblevel:m' + } + ], + supplementaryContent: [{ label: 'Finding aid', url: 'spaghetti.com' }], + creatorLiteral: [ + 'Maḥfūẓ, Najīb, 1911-2006.' + ], + creator_sort: [ + 'maḥfūẓ, najīb, 1911-2006.' + ], + level: 'debug', + items: [ + { + holdingLocation_packed: [ + 'loc:scff2||Schomburg Center - Research & Reference' + ], + suppressed: [ + false + ], + shelfMark: [ + 'Sc D 90-863' + ], + accessMessage_packed: [ + 'accessMessage:1||USE IN LIBRARY' + ], + uri: 'i10283665', + accessMessage: [ + { + label: 'USE IN LIBRARY', + id: 'accessMessage:1' + } + ], + catalogItemType: [ + { + id: 'catalogItemType:2', + label: 'book non-circ' + } + ], + deliveryLocation_packed: [ + 'loc:sc||Schomburg Center' + ], + owner: [ + { + label: 'Schomburg Center for Research in Black Culture, Jean Blackwell Hutson Research and Reference Division', + id: 'orgs:1114' + } + ], + deliveryLocation: [ + { + label: 'Schomburg Center', + id: 'loc:sc' + } + ], + identifier: [ + 'urn:barcode:32101071572406' + ], + requestable: [ + true + ], + owner_packed: [ + 'orgs:1114||Schomburg Center for Research in Black Culture, Jean Blackwell Hutson Research and Reference Division' + ], + status: [ + { + label: 'Available', + id: 'status:a' + } + ], + holdingLocation: [ + { + label: 'Schomburg Center - Research & Reference', + id: 'loc:scff2' + } + ], + status_packed: [ + 'status:a||Available' + ] + }, + { + holdingLocation_packed: [ + 'loc:scff2||Schomburg Center - Research & Reference' + ], + suppressed: [ + false + ], + shelfMark: [ + 'Sc D 90-863' + ], + accessMessage_packed: [ + 'accessMessage:1||USE IN LIBRARY' + ], + uri: 'i10283665777', + accessMessage: [ + { + label: 'USE IN LIBRARY', + id: 'accessMessage:1' + } + ], + catalogItemType: [ + { + id: 'catalogItemType:2', + label: 'book non-circ' + } + ], + deliveryLocation_packed: [ + 'loc:sc||Schomburg Center' + ], + owner: [ + { + label: 'Schomburg Center for Research in Black Culture, Jean Blackwell Hutson Research and Reference Division', + id: 'orgs:1114' + } + ], + deliveryLocation: [ + { + label: 'Schomburg Center', + id: 'loc:sc' + } + ], + identifier: [ + 'urn:barcode:32101071572406777' + ], + requestable: [ + true + ], + owner_packed: [ + 'orgs:1114||Schomburg Center for Research in Black Culture, Jean Blackwell Hutson Research and Reference Division' + ], + status: [ + { + label: 'Not Available', + id: 'status:na' + } + ], + holdingLocation: [ + { + label: 'Schomburg Center - Research & Reference', + id: 'loc:scff2' + } + ], + status_packed: [ + 'status:na||Not Available' + ] + } + ], + message: 'ResourceSerializer#serialize', + materialType_packed: [ + 'resourcetypes:txt||Text' + ], + suppressed: [ + 'false' + ], + placeOfPublication: [ + 'New York :' + ], + dateEndString: [ + '1984' + ], + title_sort: [ + 'the thief and the dogs' + ], + uris: [ + 'b11293188', + 'b11293188-i22566485', + 'b11293188-i22566489', + 'b11293188-i10283665', + 'b11293188-i10283664' + ], + language: [ + { + id: 'lang:eng', + label: 'English' + } + ], + dateString: [ + '1989' + ], + identifier: [ + 'urn:bnum:11293188', + 'urn:oclc:12248278', + 'urn:lcc:PJ7846.A46', + 'urn:lccCoarse:PJ7695.8-7976' + ], + publisher: [ + 'Doubleday,' + ], + type: [ + 'nypl:Item' + ], + createdYear: [ + 1989 + ], + contributor_sort: [ + 'badawī, muḥammad muṣṭafá.' + ], + materialType: [ + { + id: 'resourcetypes:txt', + label: 'Text' + } + ], + numAvailable: [ + 2 + ], + dimensions: [ + '22 cm.' + ], + carrierType_packed: [ + 'carriertypes:nc||volume' + ], + note: [ + 'Translation of: al-Liṣṣ wa-al-kilāb.' + ], + dateStartYear: [ + 1989 + ], + shelfMark: [ + '*OFC 90-2649' + ], + idOwi: [ + 'urn:owi:58201773' + ], + mediaType: [ + { + label: 'unmediated', + id: 'mediatypes:n' + } + ], + title: [ + 'The thief and the dogs', + 'The thief and the dogs /' + ], + titleAlt: [ + 'Liṣṣ wa-al-kilāb.' + ], + language_packed: [ + 'lang:eng||English' + ], + mediaType_packed: [ + 'mediatypes:n||unmediated' + ], + titleDisplay: [ + 'The thief and the dogs / Naguib Mahfouz ; translated by Trevor Le Gassick, M.M. Badawi ; revised by John Rodenbeck.' + ], + uri: 'b11293188', + extent: [ + '158 p. ;' + ], + carrierType: [ + { + id: 'carriertypes:nc', + label: 'volume' + } + ], + issuance_packed: [ + 'urn:biblevel:m||monograph/item' + ], + contributorLiteral: [ + 'Badawī, Muḥammad Muṣṭafá.', + 'Le Gassick, Trevor.', + 'Rodenbeck, John.' + ], + dateEndYear: [ + 1984 + ] + }, + _type: 'resource', + _index: 'resources-2017-06-13', + _score: 154.93451 + } + ] + }, + timed_out: false +} diff --git a/test/fixtures/specRequestable-es-response.js b/test/fixtures/specRequestable/specRequestable-es-response.js similarity index 99% rename from test/fixtures/specRequestable-es-response.js rename to test/fixtures/specRequestable/specRequestable-es-response.js index e2133d07..f86ef6dc 100644 --- a/test/fixtures/specRequestable-es-response.js +++ b/test/fixtures/specRequestable/specRequestable-es-response.js @@ -174,7 +174,7 @@ module.exports = () => { holdingLocation: [ { label: 'Schomburg Center - Research & Reference', - id: 'loc:scff2' + id: 'loc:mao82' } ], status_packed: [ diff --git a/test/requestability_resolver.test.js b/test/requestability_resolver.test.js index a720d7d8..da8de713 100644 --- a/test/requestability_resolver.test.js +++ b/test/requestability_resolver.test.js @@ -1,18 +1,35 @@ const RequestabilityResolver = require('../lib/requestability_resolver') const elasticSearchResponse = require('./fixtures/elastic_search_response.js') -const specRequestableElasticSearchResponse = require('./fixtures/specRequestable-es-response') +const specRequestableElasticSearchResponse = require('./fixtures/specRequestable/specRequestable-es-response.js') const eddElasticSearchResponse = require('./fixtures/edd_elastic_search_response') +const findingAidElasticSearchResponse = require('./fixtures/specRequestable/findingAid-es-response.js') const noBarcodeResponse = require('./fixtures/no_barcode_es_response') const noRecapResponse = require('./fixtures/no_recap_response') +const physRequestableOverride = require('./fixtures/specRequestable/phys-requestable-override.js') +const DeliveryLocationsResolver = require('../lib/delivery-locations-resolver.js') describe('RequestabilityResolver', () => { describe('fixItemRequestability', function () { - const NyplResponse = elasticSearchResponse.fakeElasticSearchResponseNyplItem() + let NyplResponse + before(() => { + NyplResponse = elasticSearchResponse.fakeElasticSearchResponseNyplItem() + }) it('sets physRequestable false for items with no barcodes', () => { const noBarcode = noBarcodeResponse() const resp = RequestabilityResolver.fixItemRequestability(noBarcode) expect(resp.hits.hits[0]._source.items.every((item) => item.physRequestable === false)).to.equal(true) }) + it('specRequestable overrides physRequestable, when items have phys requestable holding location', () => { + const esResponseItems = physRequestableOverride.hits.hits[0]._source.items + const isPhysRequestable = (item) => !!item.deliveryLocation.length + const resp = RequestabilityResolver.fixItemRequestability(physRequestableOverride) + // verify that items are phys requestable based on location... + expect(esResponseItems + .map(DeliveryLocationsResolver.getOnsiteDeliveryInfo) + .every(isPhysRequestable)).to.equal(true) + // ...but overridden by specRequestability + expect(resp.hits.hits[0]._source.items.every((item) => !item.physRequestable && item.specRequestable)).to.equal(true) + }) it('will set requestable to false for an item not found in ReCAP', function () { const indexedButNotAvailableInSCSBURI = 'i22566485' @@ -172,10 +189,25 @@ describe('RequestabilityResolver', () => { expect(specRequestableItem.specRequestable).to.equal(true) }) - it('marks items as not specRequestable when there is no aeonURL present', function () { + it('marks items as specRequestable when there is a special collectionAccessType designation', function () { const response = RequestabilityResolver.fixItemRequestability(specRequestableElasticSearchResponse()) const items = response.hits.hits[0]._source.items + const specRequestableItem = items.find((item) => item.uri === 'i10283665777') + expect(specRequestableItem.specRequestable).to.equal(true) + }) + + it('marks items as specRequestable when there is a finding aid on the parent bib', function () { + const response = RequestabilityResolver.fixItemRequestability(findingAidElasticSearchResponse()) + + const items = response.hits.hits[0]._source.items + expect(items.every((item) => item.specRequestable)).to.equal(true) + }) + + it('leaves item as specRequestable false when there is no finding aid, aeon url, or special holding location', () => { + const response = RequestabilityResolver.fixItemRequestability(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) + const items = response.hits.hits[0]._source.items + const specRequestableItem = items.find((item) => item.uri === 'i10283665') expect(specRequestableItem.specRequestable).to.equal(false) }) @@ -207,8 +239,13 @@ describe('RequestabilityResolver', () => { }) describe('Missing recapCustomerCode', function () { - const response = noRecapResponse.fakeElasticSearchResponseNyplItem() - const resolved = RequestabilityResolver.fixItemRequestability(response) + let response + let resolved + before(() => { + response = noRecapResponse.fakeElasticSearchResponseNyplItem() + resolved = RequestabilityResolver.fixItemRequestability(response) + }) + it('marks edd and physical requestability correctly', function () { const items = resolved.hits.hits[0]._source.items const firstItem = items.find((item) => {