Skip to content

Commit bd098d5

Browse files
authored
Merge branch 'main' into scc-5050-2
2 parents 5990165 + 2fe5063 commit bd098d5

22 files changed

+431
-79
lines changed

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "npm"
4+
directory: "/"
5+
schedule:
6+
interval: "daily"
7+
- package-ecosystem: "docker"
8+
directory: "/"
9+
schedule:
10+
interval: "weekly"

.github/workflows/test-and-deploy.yml

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,33 @@ jobs:
1515
run: npm ci
1616
- name: Unit Tests
1717
run: npm test
18-
deploy-qa:
18+
integration-test-qa:
1919
permissions:
2020
id-token: write
2121
contents: read
2222
runs-on: ubuntu-latest
2323
needs: tests
2424
if: github.ref == 'refs/heads/qa'
25+
steps:
26+
- uses: actions/checkout@v4
27+
- name: Set Node version
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version-file: '.nvmrc'
31+
- name: Install dependencies
32+
run: npm ci
33+
- name: Start service
34+
run: ENV=qa npm start &
35+
- name: Run tests
36+
run: npm run test-integration
37+
deploy-qa:
38+
permissions:
39+
id-token: write
40+
contents: read
41+
runs-on: ubuntu-latest
42+
needs:
43+
- tests
44+
if: github.ref == 'refs/heads/qa'
2545
steps:
2646
- name: Checkout repo
2747
uses: actions/checkout@v3
@@ -31,7 +51,6 @@ jobs:
3151
with:
3252
role-to-assume: arn:aws:iam::946183545209:role/GithubActionsDeployerRole
3353
aws-region: us-east-1
34-
3554
- name: Log in to ECR
3655
id: login-ecr
3756
uses: aws-actions/amazon-ecr-login@v1
@@ -60,7 +79,8 @@ jobs:
6079
id-token: write
6180
contents: read
6281
runs-on: ubuntu-latest
63-
needs: tests
82+
needs:
83+
- tests
6484
if: github.ref == 'refs/heads/qa2'
6585
steps:
6686
- name: Checkout repo

app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ app.init = async () => {
3333

3434
require('./lib/resources')(app)
3535
require('./lib/subjects')(app)
36+
require('./lib/contributors')(app)
3637
require('./lib/vocabularies')(app)
3738

3839
// routes

config/qa.env

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
ENCRYPTED_ELASTICSEARCH_URI=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAJYwgZMGCSqGSIb3DQEHBqCBhTCBggIBADB9BgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDMIkDoQ9C/cCDCAq1wIBEIBQ+L3OgUGeOW9rs1CWkhpBjwM4LbbVRFIWedqew4UXIeSNMJ8cO9SNe4YGCUIoKwCDYt7W7ip3VtDRRRMVvz6QJw+Eg8ugTMVs2pbNFGNvaAQ=
2-
RESOURCES_INDEX=resources-2025-07-07
3-
SUBJECTS_INDEX=browse-qa-2025-10-27
2+
3+
ENCRYPTED_RESOURCES_INDEX=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAHIwcAYJKoZIhvcNAQcGoGMwYQIBADBcBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDPMBVNbSFDq16QAs4AIBEIAvHrJZjGewR7g4oT5oifQUDGTj2SgYibnrhU05uBatHEVYz/mOawAVrjt/1oxPqv4=
4+
BROWSE_INDEX=browse-qa-2026-01-15
45
ENCRYPTED_ELASTICSEARCH_API_KEY=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAJ4wgZsGCSqGSIb3DQEHBqCBjTCBigIBADCBhAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAx+kryf2KUmGdBYD9sCARCAV3ygz3eXIdq8JX/wpG9JRWlTNMRcpNE1qT0zNlN4t+ZvXEoedLQa/3p1YjgHw06GIAdA9xtkMV4eH9a1K8uCvjP8XxxNKekcMj59TlResnu9QF3r7pGXuQ==
56

67
ENCRYPTED_SCSB_URL=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAH8wfQYJKoZIhvcNAQcGoHAwbgIBADBpBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDBKllElmWYLxGOGopQIBEIA8JJyKde/8m8iCJGKR5D8HoTJhXHeyvw9eIDeuUNKiXLfJwoVz+PDAZSxkCQtM9O91zGhXbe3l6Bk1RlYJ

config/test.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
ELASTICSEARCH_URI=encrypted-elasticsearch-uri
22
RESOURCES_INDEX=test-resources-index
3-
SUBJECTS_INDEX=test-subjects-index
3+
BROWSE_INDEX=test-browse-index
44

55
SCSB_URL=encrypted-scsb-url
66
SCSB_API_KEY=encrypted-scsb-api-key

lib/api-request.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ class ApiRequest {
7373
return this.params.subject_prefix
7474
}
7575

76+
hasContributorRole () {
77+
return this.params.role && this.params.filters.contributorLiteral
78+
}
79+
7680
static fromParams (params) {
7781
return new ApiRequest(params)
7882
}

lib/contributors.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
const { parseBrowseParams } = require('./elasticsearch/browse-utils')
2+
3+
const ApiRequest = require('./api-request')
4+
const ElasticQueryBrowseBuilder = require('./elasticsearch/elastic-query-browse-builder')
5+
6+
const BROWSE_INDEX = process.env.BROWSE_INDEX
7+
8+
const parseNameAndRole = (delimitedString) => {
9+
if (!delimitedString.includes('||')) {
10+
return { name: delimitedString, role: null }
11+
}
12+
const [name, role] = delimitedString.split('||')
13+
return { name, role }
14+
}
15+
16+
module.exports = function (app, _private = null) {
17+
app.contributors = {}
18+
19+
app.contributors.browse = function (params, opts, request) {
20+
app.logger.debug('Unparsed params: ', params)
21+
params = parseBrowseParams(params)
22+
23+
app.logger.debug('Parsed params: ', params)
24+
25+
const body = buildElasticContributorsBody(params)
26+
27+
app.logger.debug('Contrbutors#browse', BROWSE_INDEX, body)
28+
29+
return app.esClient.search(body, process.env.BROWSE_INDEX)
30+
.then((resp) => {
31+
return {
32+
'@type': 'contributorList',
33+
page: params.page,
34+
per_page: params.per_page,
35+
totalResults: resp.hits?.total?.value,
36+
contributors: resp.hits?.hits?.reduce((workingResponse, hit) => {
37+
if (hit.matched_queries?.[0] === 'preferredTerm' || hit.matched_queries?.[0] === 'preferredTermPrefix') { // if match is on preferredTerm, use that regardless of variant matches
38+
const { name, role } = parseNameAndRole(hit._source.preferredTerm)
39+
40+
let contributorData = workingResponse.find(item => item.termLabel === name)
41+
42+
if (!contributorData) {
43+
contributorData = {
44+
'@type': 'preferredTerm',
45+
termLabel: name
46+
}
47+
workingResponse.push(contributorData)
48+
}
49+
50+
if (role) {
51+
// just add the role count to the top level response
52+
const roleCount = { role, count: hit._source.count }
53+
if (!contributorData.roleCounts) {
54+
contributorData.roleCounts = []
55+
}
56+
contributorData.roleCounts.push(roleCount)
57+
} else {
58+
// top-level contributor object
59+
contributorData.count = hit._source.count
60+
contributorData.broaderTerms = hit._source.broaderTerms?.map((term) => ({ termLabel: term }))
61+
contributorData.narrowerTerms = hit._source.narrowerTerms?.map((term) => ({ termLabel: term }))
62+
contributorData.seeAlso = hit._source.seeAlso?.map((term) => ({ termLabel: term }))
63+
contributorData.uri = hit._source.uri
64+
}
65+
} else {
66+
// Match was on a variant- use that in the response
67+
const matchedVariantTerm = hit.inner_hits.variants.hits.hits[0]._source.variant
68+
69+
const variantData = {
70+
'@type': 'variant',
71+
termLabel: matchedVariantTerm,
72+
preferredTerms: [
73+
{
74+
termLabel: hit._source.preferredTerm,
75+
count: hit._source.count
76+
}
77+
]
78+
}
79+
80+
workingResponse.push(variantData)
81+
}
82+
83+
return workingResponse
84+
}, [])
85+
}
86+
})
87+
}
88+
89+
// For unit testing
90+
if (_private && typeof _private === 'object') {
91+
_private.buildElasticContributorsBody = buildElasticContributorsBody
92+
_private.parseBrowseParams = parseBrowseParams
93+
}
94+
}
95+
96+
/**
97+
* Given GET params, returns a plainobject with `from`, `size`, `query`,
98+
* `sort`, and any other params necessary to perform the ES query based
99+
* on the GET params.
100+
*
101+
* @return {object} An object that can be posted directly to ES
102+
*/
103+
const buildElasticContributorsBody = function (params) {
104+
const body = {
105+
from: (params.per_page * (params.page - 1)),
106+
size: params.per_page
107+
}
108+
109+
const request = ApiRequest.fromParams(params)
110+
const builder = ElasticQueryBrowseBuilder.forApiRequest(request)
111+
112+
body.query = builder.query.toJson()
113+
114+
// match only termType 'contributor'
115+
body.query.bool.must.push({ term: { termType: { value: 'contributor' } } })
116+
117+
// Exclude items that have count == 0
118+
body.query.bool.must.push({ range: { count: { gt: 0 } } })
119+
120+
return body
121+
}

lib/elasticsearch/browse-utils.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const { parseParams } = require('../util')
2+
3+
// Default sort orders for different search scopes
4+
const SEARCH_SCOPE_SORT_ORDER = {
5+
has: 'count',
6+
starts_with: 'termLabel'
7+
}
8+
9+
const SEARCH_SCOPES = [
10+
'has',
11+
'starts_with'
12+
]
13+
14+
const SORT_FIELDS = [
15+
'termLabel',
16+
'count',
17+
'relevance'
18+
]
19+
20+
exports.parseBrowseParams = function (params) {
21+
return parseParams(params, {
22+
q: { type: 'string' },
23+
page: { type: 'int', default: 1 },
24+
per_page: { type: 'int', default: 50, range: [0, 100] },
25+
sort: { type: 'string', range: SORT_FIELDS, default: SEARCH_SCOPE_SORT_ORDER[params.search_scope] || 'termLabel' },
26+
sort_direction: { type: 'string', range: ['asc', 'desc'] },
27+
search_scope: { type: 'string', range: SEARCH_SCOPES, default: '' }
28+
})
29+
}

lib/elasticsearch/config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,6 @@ const AGGREGATIONS_SPEC = {
124124
buildingLocation: { terms: { field: 'buildingLocationIds' } },
125125
subjectLiteral: { terms: { field: 'subjectLiteral.raw' } },
126126
language: { terms: { field: 'language_packed' } },
127-
contributorLiteral: { terms: { field: 'contributorLiteral.raw' } },
128-
creatorLiteral: { terms: { field: 'creatorLiteral.raw' } },
129127
collection: { terms: { field: 'collectionIds' } }
130128
}
131129

lib/elasticsearch/elastic-query-subjects-builder.js renamed to lib/elasticsearch/elastic-query-browse-builder.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const ElasticQuery = require('./elastic-query')
22

3-
class ElasticQuerySubjectsBuilder {
3+
class ElasticQueryBrowseBuilder {
44
constructor (apiRequest) {
55
this.request = apiRequest
66
this.query = new ElasticQuery()
@@ -53,7 +53,6 @@ class ElasticQuerySubjectsBuilder {
5353
bool: {
5454
should: [
5555
{ match: { preferredTerm: { query: this.request.querySansQuotes(), operator: 'and', _name: 'preferredTerm' } } },
56-
{ prefix: { preferredTerm: { value: this.request.querySansQuotes(), _name: 'preferredTermPrefix' } } },
5756
{ nested: { path: 'variants', query: { match: { 'variants.variant': { query: this.request.querySansQuotes(), operator: 'and' } } }, inner_hits: {} } }
5857
]
5958
}
@@ -64,8 +63,8 @@ class ElasticQuerySubjectsBuilder {
6463
* Create a ElasticQueryBuilder for given ApiRequest instance
6564
*/
6665
static forApiRequest (request) {
67-
return new ElasticQuerySubjectsBuilder(request)
66+
return new ElasticQueryBrowseBuilder(request)
6867
}
6968
}
7069

71-
module.exports = ElasticQuerySubjectsBuilder
70+
module.exports = ElasticQueryBrowseBuilder

0 commit comments

Comments
 (0)