-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
move pubrouter function to api codebase, add tests
- Loading branch information
1 parent
2706920
commit 784107b
Showing
17 changed files
with
343 additions
and
348 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -113,6 +113,8 @@ services: | |
- TRIGGER_ARI_INGEST_API_KEY=123456789 | ||
- [email protected] | ||
- PARTICIPATING_ARI_USER_IDS= | ||
- PUBROUTER_API_KEYS | ||
- [email protected] | ||
|
||
volumes: | ||
opensearch-data1: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Config only used locally | ||
functions: | ||
notifyPubRouter: | ||
handler: dist/src/components/pubRouter/controller.notifyPubRouter | ||
events: | ||
- http: | ||
path: ${self:custom.versions.v1}/publications/{publicationId}/notifyPubRouter | ||
method: POST | ||
cors: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
api/src/components/pubRouter/__tests__/notifyPubRouter.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import * as testUtils from 'lib/testUtils'; | ||
|
||
beforeAll(async () => { | ||
await testUtils.clearDB(); | ||
await testUtils.testSeed(); | ||
}); | ||
|
||
describe('Notify pubRouter of a publication', () => { | ||
test('Notify of existing non-seed publication', async () => { | ||
const response = await testUtils.agent.post('/publications/publication-problem-live/notifyPubRouter'); | ||
|
||
expect(response.status).toEqual(200); | ||
expect(response.body.message).toEqual('Successfully submitted to publications router'); | ||
}); | ||
|
||
test('Notify of seed publication', async () => { | ||
const response = await testUtils.agent.post('/publications/seed-publication/notifyPubRouter'); | ||
|
||
expect(response.status).toEqual(200); | ||
expect(response.body.message).toEqual('Publication author is Octopus user, ignoring'); | ||
}); | ||
|
||
test('Notify of non-existent publication', async () => { | ||
const response = await testUtils.agent.post('/publications/made-up-publication/notifyPubRouter'); | ||
|
||
expect(response.status).toEqual(404); | ||
expect(response.body.message).toEqual('Publication version not found.'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { S3CreateEvent } from 'aws-lambda'; | ||
|
||
import * as Helpers from 'lib/helpers'; | ||
import * as I from 'interface'; | ||
import * as pubRouterService from './service'; | ||
import * as publicationVersionService from 'publicationVersion/service'; | ||
import * as response from 'lib/response'; | ||
import * as s3 from 'lib/s3'; | ||
|
||
/** | ||
* Fires a notification to the PubRouter API with details of a new publication when it has a PDF generated. | ||
* @param event When a file is deposited into the PDF S3 bucket (on deployed environments), or when | ||
* a request is made to the API to trigger this function (locally). | ||
* @returns A response object with a status code and message. | ||
*/ | ||
export const notifyPubRouter = async ( | ||
event: S3CreateEvent | I.APIRequest<undefined, undefined, I.LocalNotifyPubRouterPathParams> | ||
): Promise<I.JSONResponse> => { | ||
let publicationId: string; | ||
let pdfUrl: string; | ||
|
||
// If this is running locally, get details from lambda event. | ||
if (process.env.STAGE === 'local' && Object.hasOwn(event, 'pathParameters')) { | ||
const lambdaEvent = event as I.APIRequest<undefined, undefined, I.LocalNotifyPubRouterPathParams>; | ||
publicationId = lambdaEvent.pathParameters.publicationId; | ||
pdfUrl = Helpers.checkEnvVariable('LOCALSTACK_SERVER') + s3.buckets.pdfs + `/${publicationId}.pdf`; | ||
} else { | ||
// Otherwise, the "real" way, get details from S3 event. | ||
const s3Event = event as S3CreateEvent; | ||
const bucket = s3Event.Records[0].s3.bucket.name; | ||
const key = s3Event.Records[0].s3.object.key; | ||
pdfUrl = `https://${bucket}.s3.amazonaws.com/${key}`; | ||
publicationId = key.replace(/\.pdf$/, ''); | ||
} | ||
|
||
try { | ||
const publicationVersion = await publicationVersionService.get(publicationId, 'latest'); | ||
|
||
if (!publicationVersion) { | ||
return response.json(404, { | ||
message: 'Publication version not found.' | ||
}); | ||
} | ||
|
||
// If publication was written by Science Octopus (seed data), don't send. | ||
if (publicationVersion.user && publicationVersion.user.id === 'octopus') { | ||
console.log('Publication author is Octopus user, ignoring'); | ||
|
||
return response.json(200, { message: 'Publication author is Octopus user, ignoring' }); | ||
} | ||
|
||
const { code, message } = await pubRouterService.notifyPubRouter(publicationVersion, pdfUrl); | ||
|
||
return response.json(code, { message }); | ||
} catch (err) { | ||
console.log(err); | ||
|
||
return response.json(500, { message: 'Unknown server error.' }); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import * as email from 'lib/email'; | ||
import * as Helpers from 'lib/helpers'; | ||
import * as I from 'interface'; | ||
|
||
const pubRouterPublicationTypePrefix = 'Octopus article; '; | ||
|
||
const getPubRouterMetadata = (publicationVersion: I.PublicationVersion, pdfUrl: string) => { | ||
const formatCoAuthor = (coAuthor: I.PublicationVersion['coAuthors'][0]) => { | ||
if (!coAuthor.user) return null; | ||
|
||
return { | ||
type: coAuthor.linkedUser === publicationVersion.user.id ? 'corresp' : 'author', | ||
name: { | ||
firstname: coAuthor.user.firstName, | ||
surname: coAuthor.user.lastName || '', | ||
fullname: `${coAuthor.user.lastName && coAuthor.user.lastName + ', '}${coAuthor.user.firstName}` | ||
}, | ||
identifier: [ | ||
{ | ||
type: 'orcid', | ||
id: coAuthor.user.orcid | ||
} | ||
], | ||
affiliations: coAuthor.affiliations?.map((rawAffiliationJson) => { | ||
const affiliation: I.MappedOrcidAffiliation = JSON.parse(rawAffiliationJson as string); | ||
|
||
return { | ||
identifier: [ | ||
...(affiliation.organization['disambiguated-organization'] && | ||
affiliation.organization['disambiguated-organization']['disambiguation-source'] === 'ROR' | ||
? [ | ||
{ | ||
type: 'ROR', | ||
id: affiliation.organization['disambiguated-organization'][ | ||
'disambiguated-organization-identifier' | ||
] | ||
} | ||
] | ||
: []), | ||
...(affiliation.url | ||
? [ | ||
{ | ||
type: 'Link', | ||
id: affiliation.url | ||
} | ||
] | ||
: []) | ||
], | ||
org: affiliation.organization.name, | ||
city: affiliation.organization.address.city, | ||
country: affiliation.organization.address.country | ||
}; | ||
}) | ||
}; | ||
}; | ||
|
||
const formattedPublicationDate = publicationVersion.createdAt.toISOString().split('T')[0]; | ||
const publication = publicationVersion.publication; | ||
const formattedCoAuthors = publicationVersion.coAuthors?.map((coAuthor) => formatCoAuthor(coAuthor)); | ||
|
||
return { | ||
provider: { | ||
agent: 'Octopus' | ||
}, | ||
links: [ | ||
{ | ||
format: 'application/pdf', | ||
url: pdfUrl | ||
} | ||
], | ||
metadata: { | ||
journal: { | ||
title: 'Octopus', | ||
publisher: ['Octopus'], | ||
identifier: [ | ||
{ | ||
type: 'doi', | ||
id: 'https://doi.org/10.57874/OCTOPUS' | ||
} | ||
] | ||
}, | ||
article: { | ||
title: publicationVersion.title, | ||
abstract: publicationVersion.description, | ||
type: `${pubRouterPublicationTypePrefix}${publicationVersion.publication.type}`, | ||
version: 'SMUR', | ||
language: [publicationVersion.language], | ||
identifier: [ | ||
{ | ||
type: 'doi', | ||
id: publication.doi | ||
} | ||
], | ||
e_num: publication.id // DOI suffix e.g. "abcd-efgh" | ||
}, | ||
author: formattedCoAuthors, | ||
publication_date: { | ||
date: formattedPublicationDate, | ||
year: formattedPublicationDate.slice(0, 4), | ||
month: formattedPublicationDate.slice(5, 7), | ||
day: formattedPublicationDate.slice(8, 10) | ||
}, | ||
accepted_date: formattedPublicationDate, | ||
publication_status: 'Published', | ||
funding: publicationVersion.funders?.map((funder) => ({ | ||
name: funder.name, | ||
...(funder.ror && { | ||
identifier: [ | ||
{ | ||
type: 'ror', | ||
id: funder.ror | ||
} | ||
] | ||
}), | ||
...(funder.grantId && { grant_numbers: [funder.grantId] }) | ||
})), | ||
license_ref: [ | ||
{ | ||
title: 'CC-BY 4.0', | ||
type: 'CC-BY 4.0', | ||
url: 'https://creativecommons.org/licenses/by/4.0/' | ||
} | ||
], | ||
peer_reviewed: false | ||
} | ||
}; | ||
}; | ||
|
||
const postToPubRouter = async (pdfMetadata: ReturnType<typeof getPubRouterMetadata>, endpoint: string) => | ||
fetch(endpoint, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: JSON.stringify(pdfMetadata) | ||
}); | ||
|
||
export const notifyPubRouter = async ( | ||
publicationVersion: I.PublicationVersion, | ||
pdfUrl: string | ||
): Promise<{ code: number; message: string }> => { | ||
const pdfMetadata = getPubRouterMetadata(publicationVersion, pdfUrl); | ||
console.log('PDF metadata: ', JSON.stringify(pdfMetadata)); | ||
|
||
// Send to PubRouter. | ||
const publicationType = pdfMetadata.metadata.article.type.replace(pubRouterPublicationTypePrefix, ''); | ||
// We use a different API key per publication type. | ||
const apiKeys = JSON.parse(Helpers.checkEnvVariable('PUBROUTER_API_KEYS')); | ||
|
||
if (!Object.keys(apiKeys).includes(publicationType)) { | ||
throw new Error(`Publication type "${publicationType}" not found in API keys object`); | ||
} else { | ||
const apiKey = apiKeys[publicationType]; | ||
// Hit UAT endpoint if not running on prod. | ||
const apiEndpoint = `https://${ | ||
process.env.STAGE !== 'prod' ? 'uat.' : '' | ||
}pubrouter.jisc.ac.uk/api/v4/notification?api_key=${apiKey}`; | ||
|
||
try { | ||
const response = await postToPubRouter(pdfMetadata, apiEndpoint); | ||
// Check the API response and handle failures | ||
const successResponse = { code: 200, message: 'Successfully submitted to publications router' }; | ||
|
||
if (response.ok) { | ||
return successResponse; | ||
} else { | ||
// Retry once | ||
console.log('First attempt failed; retrying'); | ||
const retry = await postToPubRouter(pdfMetadata, apiEndpoint); | ||
|
||
if (retry.ok) { | ||
return successResponse; | ||
} else { | ||
const retryJSON = await retry.json(); | ||
await email.pubRouterFailure(publicationVersion.versionOf, JSON.stringify(retryJSON)); | ||
|
||
return { code: 500, message: 'Failed to submit to publications router' }; | ||
} | ||
} | ||
} catch (error) { | ||
const errorString = error.toString(); | ||
await email.pubRouterFailure(publicationVersion.versionOf, errorString); | ||
|
||
return { code: 500, message: errorString }; | ||
} | ||
} | ||
}; |
Oops, something went wrong.