Skip to content

Commit f917024

Browse files
authored
Merge pull request #624 from NYPL/qa
Qa to prod: MARC endpoint
2 parents 6404e6a + cac891f commit f917024

File tree

9 files changed

+475
-32
lines changed

9 files changed

+475
-32
lines changed

data/marc-rules.json

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
[
2+
{
3+
"marcIndicatorRegExp": "3610 ",
4+
"subfieldSpec": { "subfields": [], "directive": "include" },
5+
"label": "",
6+
"directive": "exclude"
7+
},
8+
{
9+
"marcIndicatorRegExp": "365..",
10+
"subfieldSpec": { "subfields": [], "directive": "include" },
11+
"label": "",
12+
"directive": "exclude"
13+
},
14+
{
15+
"marcIndicatorRegExp": "5410.",
16+
"subfieldSpec": { "subfields": [], "directive": "include" },
17+
"label": "",
18+
"directive": "exclude"
19+
},
20+
{
21+
"marcIndicatorRegExp": "541 .",
22+
"subfieldSpec": { "subfields": [], "directive": "include" },
23+
"label": "",
24+
"directive": "exclude"
25+
},
26+
{
27+
"marcIndicatorRegExp": "5420 ",
28+
"subfieldSpec": { "subfields": [], "directive": "include" },
29+
"label": "",
30+
"directive": "exclude"
31+
},
32+
{
33+
"marcIndicatorRegExp": "5610.",
34+
"subfieldSpec": { "subfields": [], "directive": "include" },
35+
"label": "",
36+
"directive": "exclude"
37+
},
38+
{
39+
"marcIndicatorRegExp": "561 .",
40+
"subfieldSpec": { "subfields": [], "directive": "include" },
41+
"label": "",
42+
"directive": "exclude"
43+
},
44+
{
45+
"marcIndicatorRegExp": "5830.",
46+
"subfieldSpec": { "subfields": [], "directive": "include" },
47+
"label": "",
48+
"directive": "exclude"
49+
},
50+
{
51+
"marcIndicatorRegExp": "583 .",
52+
"subfieldSpec": { "subfields": [], "directive": "include" },
53+
"label": "",
54+
"directive": "exclude"
55+
},
56+
{
57+
"marcIndicatorRegExp": "5900.",
58+
"subfieldSpec": { "subfields": [], "directive": "include" },
59+
"label": "",
60+
"directive": "exclude"
61+
}
62+
]

lib/annotated-marc-serializer.js

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
const arrayUnique = require('./util').arrayUnique
3939
const relatorMappings = require('../data/relator-mappings.json')
40+
const { varFieldMatches, buildSourceWithMasking } = require('./marc-util')
4041

4142
class AnnotatedMarcSerializer {
4243
}
@@ -133,32 +134,13 @@ AnnotatedMarcSerializer.matchingMarcFields = function (bib, rule) {
133134
*
134135
* @return {boolean}
135136
*/
136-
AnnotatedMarcSerializer.varFieldMatches = function (field, rule) {
137-
const fieldMarcIndicator = `${field.marcTag}${field.ind1 || ' '}${field.ind2 || ' '}`
138-
return rule.marcIndicatorRegExp.test(fieldMarcIndicator) &&
139-
rule.fieldTag === field.fieldTag
140-
}
137+
AnnotatedMarcSerializer.varFieldMatches = varFieldMatches
141138

142139
/**
143140
* Given a varField, returns a copy with any hidden subfield content replaced
144141
* with "[redacted]" based on given rule
145142
*/
146-
AnnotatedMarcSerializer.buildSourceWithMasking = function (field, rule) {
147-
return Object.assign({}, field, {
148-
subfields: (field.subfields || [])
149-
.map((subfield) => {
150-
let subfieldContent = subfield.content
151-
// If directive is 'include' and subfield not included
152-
// .. or directive is 'exclude', but subfield included,
153-
// [redact] it:
154-
if ((rule.subfieldSpec.directive === 'include' && rule.subfieldSpec.subfields.indexOf(subfield.tag) < 0) ||
155-
(rule.subfieldSpec.directive === 'exclude' && rule.subfieldSpec.subfields.indexOf(subfield.tag) >= 0)) {
156-
subfieldContent = '[redacted]'
157-
}
158-
return Object.assign({}, subfield, { content: subfieldContent })
159-
})
160-
})
161-
}
143+
AnnotatedMarcSerializer.buildSourceWithMasking = buildSourceWithMasking
162144

163145
/**
164146
* Get prefix for a marctag & subfield, given a previous subfield (if avail.)

lib/marc-serializer.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* @typedef {object} MarcRuleSubfieldSpec
3+
* @property {array<string>} subfields - Array of subfields to match for suppression
4+
* @property {string} directive - Indicates whether the matching subfields
5+
* should be "include"d or "exclude"d
6+
*/
7+
/**
8+
* @typedef {object} MarcRule
9+
* @property {string} fieldTag - Single character tag broadly classifying tag (e.g. 'y')
10+
* @property {string} marcIndicatorRegExp - Stringified regex for matching a
11+
* VarField tag joined to 1st and 2nd indicators
12+
* @property {MarcRuleSubfieldSpec} subfieldSpec - How to match subfields
13+
* @property {string} directive - Whether to include/exclude if matched.
14+
*/
15+
16+
/**
17+
* @typedef {object} SubField
18+
* @property {string} tag - Identifying tag (e.g. '6', 'a')
19+
* @property {string} content - Value of subfield
20+
*/
21+
22+
/**
23+
* @typedef {object} VarField
24+
* * @property {string} marcTag - Three digit number classifying field (e.g. '100')
25+
* @property {string} fieldTag - Single character tag broadly classifying tag (e.g. 'y')
26+
* @property {string} content - Root level content (usually null/ignored)
27+
* @property {array<SubField>} subfields
28+
* @property {string|null} ind1 - First indicator character (space if blank)
29+
* @property {string|null} ind2 - Second indicator character (space if blank)
30+
*/
31+
32+
/**
33+
* @typedef {object} SerializedBib
34+
* @property {string} id - Bib ID
35+
* @property {string} nyplSource - MARC source
36+
* @property {array<VarField>} fields - Array of varFields after suppression
37+
*/
38+
39+
/**
40+
* @typedef {object} SerializedMarc
41+
* @property {SerializedBib} bib - The serialized bib object containing varFields
42+
*/
43+
44+
const { varFieldMatches } = require('./marc-util')
45+
46+
class MarcSerializer {}
47+
48+
// Load rules
49+
MarcSerializer.mappingRules = require('../data/marc-rules.json')
50+
.map((rule) => {
51+
return Object.assign({}, rule, {
52+
marcIndicatorRegExp: new RegExp(rule.marcIndicatorRegExp)
53+
})
54+
})
55+
56+
/**
57+
* Returns true if a field matches a given MARC rule
58+
* @param {VarField} field - MARC field to test
59+
* @param {MarcRule} rule - Rule to match against
60+
* @returns {boolean}
61+
*/
62+
MarcSerializer.varFieldMatches = varFieldMatches
63+
64+
MarcSerializer.describeField = function (field) {
65+
return `${field.marcTag}${field.ind1 || ' '}${field.ind2 || ' '}`
66+
}
67+
68+
/**
69+
* Finds linked 880 fields (parallel scripts) for a given field
70+
* @param {Bib} bib - Bib object containing varFields
71+
* @param {VarField} sourceField - Field to find parallels for
72+
* @returns {Array<VarField>} Array of parallel 880 fields
73+
*/
74+
MarcSerializer.findParallelFields = function (bib, sourceField) {
75+
const linkNumbers = extractLinkingNumbers(sourceField)
76+
if (linkNumbers.length === 0) return []
77+
78+
return bib.varFields.filter((field) =>
79+
isLinked880Field(field, linkNumbers)
80+
)
81+
}
82+
83+
/**
84+
* Extracts linking numbers from subfield 6, removing the 880- prefix
85+
*/
86+
function extractLinkingNumbers (varField) {
87+
return (varField.subfields || [])
88+
// Is a MARC linking subfield ($6)?
89+
.filter((subfield) => subfield.tag === '6')
90+
.map((subfield) => subfield.content.replace(/^880-/, ''))
91+
}
92+
93+
/**
94+
* Determines whether a field is an 880 field linked to any of the given numbers
95+
*/
96+
function isLinked880Field (field, linkNumbers) {
97+
if (field.marcTag !== '880' || !field.subfields) return false
98+
99+
const fieldLinks = field.subfields
100+
// Is a MARC linking subfield ($6)?
101+
.filter((subfield) => subfield.tag === '6')
102+
.map((subfield) => subfield.content)
103+
104+
return fieldLinks.some((link) =>
105+
linkNumbers.some((linkNumber) => isMatchingLink(link, linkNumber))
106+
)
107+
}
108+
109+
/**
110+
* Checks whether a link contains the link number at position 4
111+
*/
112+
function isMatchingLink (link, linkNumber) {
113+
return link.indexOf(linkNumber) === 4
114+
}
115+
116+
/**
117+
* Serializes a bib with excluded fields
118+
* @param {Bib} bib - Bib to serialize
119+
* @returns {SerializedMarc} Serialized bib
120+
*/
121+
MarcSerializer.serialize = function (bib) {
122+
// Keep track of 880 parallels to exclude
123+
const excludedLinkNumbers = new Set()
124+
125+
const serializedVarFields = bib.varFields.filter((field) => {
126+
// Check if this 880 field is linked to an excluded source
127+
if (field.marcTag === '880') {
128+
const fieldLinks = field.subfields
129+
.filter(sf => sf.tag === '6')
130+
.map(sf => sf.content)
131+
132+
const shouldExclude = fieldLinks.some(link =>
133+
Array.from(excludedLinkNumbers).some(ln =>
134+
link.indexOf(ln) === 4
135+
)
136+
)
137+
138+
if (shouldExclude) return false
139+
}
140+
141+
// Find matching rule for this field
142+
const matchingRule = MarcSerializer.mappingRules.find((rule) =>
143+
MarcSerializer.varFieldMatches(field, rule)
144+
)
145+
146+
if (!matchingRule) return true
147+
148+
// If field is excluded, mark its link numbers for excluding 880 parallels
149+
if (matchingRule.directive === 'exclude') {
150+
const linkNumbers = extractLinkingNumbers(field)
151+
linkNumbers.forEach((ln) => excludedLinkNumbers.add(ln))
152+
return false
153+
}
154+
155+
// Otherwise, keep the field
156+
return true
157+
})
158+
159+
return {
160+
bib: {
161+
id: bib.id,
162+
nyplSource: bib.nyplSource,
163+
fields: serializedVarFields
164+
}
165+
}
166+
}
167+
168+
module.exports = MarcSerializer

lib/marc-util.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* * Returns true if a field matches a given MARC rule
3+
* @param {VarField} field
4+
* @param {MarcRule} rule
5+
* @returns {boolean}
6+
*/
7+
function varFieldMatches (field, rule) {
8+
const indicator = `${field.marcTag || ''}${field.ind1 || ' '}${field.ind2 || ' '}`
9+
10+
if (rule.fieldTag && rule.fieldTag !== field.fieldTag) {
11+
return false
12+
}
13+
14+
return rule.marcIndicatorRegExp.test(indicator)
15+
}
16+
17+
/**
18+
* Returns a copy of a varField with removed subfields according to the rule
19+
* @param {VarField} field
20+
* @param {MarcRule} rule
21+
* @returns {VarField}
22+
*/
23+
function buildSourceWithMasking (field, rule) {
24+
return {
25+
...field,
26+
subfields: (field.subfields || []).filter((subfield) => {
27+
if (
28+
(rule.subfieldSpec.directive === 'include' &&
29+
!rule.subfieldSpec.subfields.includes(subfield.tag)) ||
30+
(rule.subfieldSpec.directive === 'exclude' &&
31+
rule.subfieldSpec.subfields.includes(subfield.tag))
32+
) {
33+
return false
34+
}
35+
return true
36+
})
37+
}
38+
}
39+
40+
module.exports = {
41+
varFieldMatches,
42+
buildSourceWithMasking
43+
}

lib/models/Location.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Location {
99
}
1010

1111
get deliverableToResolution () {
12-
if (this.nyplCoreLocation) {
12+
if (this.nyplCoreLocation?.deliverableToResolution) {
1313
return this.nyplCoreLocation.deliverableToResolution
1414
} else if (this.recapCustomerCode) return 'recap-customer-code'
1515
}

lib/resources.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const AggregationSerializer = require('./jsonld_serializers.js').AggregationSeri
88
const ItemResultsSerializer = require('./jsonld_serializers.js').ItemResultsSerializer
99
const LocationLabelUpdater = require('./location_label_updater')
1010
const AnnotatedMarcSerializer = require('./annotated-marc-serializer')
11+
const MarcSerializer = require('./marc-serializer')
1112
const { makeNyplDataApiClient } = require('./data-api-client')
1213
const { IndexSearchError, IndexConnectionError } = require('./errors')
1314

@@ -231,6 +232,30 @@ module.exports = function (app, _private = null) {
231232
.then(AnnotatedMarcSerializer.serialize)
232233
}
233234

235+
// Get a single raw marc:
236+
app.resources.marc = async function (params, opts) {
237+
// Convert discovery id to nyplSource and un-prefixed id:
238+
const nyplSourceMapper = await NyplSourceMapper.instance()
239+
const { id, nyplSource } = nyplSourceMapper.splitIdentifier(params.uri) ?? {}
240+
241+
if (!id || !nyplSource) {
242+
throw new errors.InvalidParameterError(`Invalid bnum: ${params.uri}`)
243+
}
244+
245+
app.logger.debug('Resources#marc', { id, nyplSource })
246+
247+
return makeNyplDataApiClient().get(`bibs/${nyplSource}/${id}`)
248+
.then((resp) => {
249+
// need to check that the query actually found an entry
250+
if (!resp.data) {
251+
throw new errors.NotFoundError(`Record not found: bibs/${nyplSource}/${id}`)
252+
} else {
253+
return resp.data
254+
}
255+
})
256+
.then(MarcSerializer.serialize)
257+
}
258+
234259
function itemsByFilter (filter, opts) {
235260
opts = Object.assign({
236261
_source: null

routes/resources.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ module.exports = function (app) {
106106

107107
if (req.params.ext === 'annotated-marc') {
108108
handler = app.resources.annotatedMarc
109+
} else if (req.params.ext === 'marc') {
110+
handler = app.resources.marc
109111
}
110112

111113
return handler(params, { baseUrl: app.baseUrl }, req)

0 commit comments

Comments
 (0)