diff --git a/api/serverless.yml b/api/serverless.yml index b321e993e..9d25232f0 100644 --- a/api/serverless.yml +++ b/api/serverless.yml @@ -148,13 +148,6 @@ functions: seedOpenSearch: handler: src/lib/testUtils.openSearchSeed # Publications - getPublications: - handler: src/components/publication/routes.getAll - events: - - http: - path: ${self:custom.versions.v1}/publications - method: GET - cors: true getPublication: handler: src/components/publication/routes.get events: @@ -176,27 +169,6 @@ functions: path: ${self:custom.versions.v1}/publications method: POST cors: true - updateStatus: - handler: src/components/publication/routes.updateStatus - events: - - http: - path: ${self:custom.versions.v1}/publications/{id}/status/{status} - method: PUT - cors: true - updatePublication: - handler: src/components/publication/routes.update - events: - - http: - path: ${self:custom.versions.v1}/publications/{id} - method: PATCH - cors: true - deletePublication: - handler: src/components/publication/routes.deletePublication - events: - - http: - path: ${self:custom.versions.v1}/publications/{id} - method: DELETE - cors: true getPublicationLinks: handler: src/components/publication/routes.getPublicationLinks events: @@ -219,6 +191,49 @@ functions: path: ${self:custom.versions.v1}/publications/research-topics method: GET cors: true + getPublicationTopics: + handler: src/components/publication/routes.getPublicationTopics + events: + - http: + path: ${self:custom.versions.v1}/publications/{id}/topics + method: GET + cors: true + # Publication Versions + getPublicationVersion: + handler: src/components/publicationVersion/routes.get + events: + - http: + path: ${self:custom.versions.v1}/publications/{id}/publication-versions/{version} # {version} can be a version id, number or "latest" + method: GET + cors: true + getIndexedPublicationVersions: + handler: src/components/publicationVersion/routes.getAll + events: + - http: + path: ${self:custom.versions.v1}/publication-versions + method: GET + cors: true + updatePublicationVersion: + handler: src/components/publicationVersion/routes.update + events: + - http: + path: ${self:custom.versions.v1}/publication-versions/{id} + method: PATCH + cors: true + updateStatus: + handler: src/components/publicationVersion/routes.updateStatus + events: + - http: + path: ${self:custom.versions.v1}/publication-versions/{id}/status/{status} + method: PUT + cors: true + deleteVersion: + handler: src/components/publicationVersion/routes.deleteVersion + events: + - http: + path: ${self:custom.versions.v1}/publication-versions/{id} + method: DELETE + cors: true # Users getUsers: handler: src/components/user/routes.getAll @@ -234,11 +249,11 @@ functions: path: ${self:custom.versions.v1}/users/{id} method: GET cors: true - getUserPublications: - handler: src/components/user/routes.getPublications + getUserPublicationVersions: + handler: src/components/user/routes.getPublicationVersions events: - http: - path: ${self:custom.versions.v1}/users/{id}/publications + path: ${self:custom.versions.v1}/users/{id}/publication-versions method: GET cors: true @@ -298,91 +313,91 @@ functions: handler: src/components/coauthor/routes.get events: - http: - path: ${self:custom.versions.v1}/publicationVersions/{id}/coauthors + path: ${self:custom.versions.v1}/publication-versions/{id}/coauthors method: GET cors: true updateAll: handler: src/components/coauthor/routes.updateAll events: - http: - path: ${self:custom.versions.v1}/publicationVersions/{id}/coauthors + path: ${self:custom.versions.v1}/publication-versions/{id}/coauthors method: PUT cors: true deleteCoAuthor: handler: src/components/coauthor/routes.remove events: - http: - path: ${self:custom.versions.v1}/publicationVersions/{id}/coauthors/{coauthor} + path: ${self:custom.versions.v1}/publication-versions/{id}/coauthors/{coauthor} method: DELETE cors: true linkCoAuthor: handler: src/components/coauthor/routes.link events: - http: - path: ${self:custom.versions.v1}/publicationVersions/{id}/link-coauthor + path: ${self:custom.versions.v1}/publication-versions/{id}/link-coauthor method: PATCH cors: true updateCoAuthorConfirmation: handler: src/components/coauthor/routes.updateConfirmation events: - http: - path: ${self:custom.versions.v1}/publicationVersions/{id}/coauthor-confirmation + path: ${self:custom.versions.v1}/publication-versions/{id}/coauthor-confirmation method: PATCH cors: true requestApproval: handler: src/components/coauthor/routes.requestApproval events: - http: - path: ${self:custom.versions.v1}/publicationVersions/{id}/coauthors/request-approval + path: ${self:custom.versions.v1}/publication-versions/{id}/coauthors/request-approval method: PUT cors: true sendApprovalReminder: handler: src/components/coauthor/routes.sendApprovalReminder events: - http: - path: ${self:custom.versions.v1}/publicationVersions/{id}/coauthors/{coauthor}/approval-reminder + path: ${self:custom.versions.v1}/publication-versions/{id}/coauthors/{coauthor}/approval-reminder method: POST cors: true getFlag: handler: src/components/flag/routes.get events: - http: - path: ${self:custom.versions.v1}/flag/{id} + path: ${self:custom.versions.v1}/flags/{id} method: GET cors: true createFlag: handler: src/components/flag/routes.createFlag events: - http: - path: ${self:custom.versions.v1}/publications/{id}/flag + path: ${self:custom.versions.v1}/publications/{id}/flags method: POST cors: true createFlagComment: handler: src/components/flag/routes.createFlagComment events: - http: - path: ${self:custom.versions.v1}/flag/{id}/comment + path: ${self:custom.versions.v1}/flags/{id}/comment method: POST cors: true resolveFlag: handler: src/components/flag/routes.resolveFlag events: - http: - path: ${self:custom.versions.v1}/flag/{id}/resolve + path: ${self:custom.versions.v1}/flags/{id}/resolve method: POST cors: true getPublicationFlags: handler: src/components/flag/routes.getPublicationFlags events: - http: - path: ${self:custom.versions.v1}/flag/publication/{id} + path: ${self:custom.versions.v1}/publications/{id}/flags method: GET cors: true getUserFlags: handler: src/components/flag/routes.getUserFlags events: - http: - path: ${self:custom.versions.v1}/flag/user/{id} + path: ${self:custom.versions.v1}/user/{id}/flags method: GET cors: true # Images @@ -448,14 +463,14 @@ functions: handler: src/components/funder/routes.create events: - http: - path: ${self:custom.versions.v1}/publications/{id}/funders + path: ${self:custom.versions.v1}/publication-versions/{id}/funders method: POST cors: true deleteFunder: handler: src/components/funder/routes.destroy events: - http: - path: ${self:custom.versions.v1}/publications/{id}/funders/{funder} + path: ${self:custom.versions.v1}/publication-versions/{id}/funders/{funder} method: DELETE cors: true # References @@ -463,14 +478,14 @@ functions: handler: src/components/reference/routes.get events: - http: - path: ${self:custom.versions.v1}/publicationVersions/{id}/reference + path: ${self:custom.versions.v1}/publication-versions/{id}/references method: get cors: true updateAllReference: handler: src/components/reference/routes.updateAll events: - http: - path: ${self:custom.versions.v1}/publicationVersions/{id}/reference + path: ${self:custom.versions.v1}/publication-versions/{id}/references method: put cors: true @@ -479,7 +494,7 @@ functions: handler: src/components/affiliations/routes.updateAffiliations events: - http: - path: ${self:custom.versions.v1}/publicationVersions/{id}/my-affiliations + path: ${self:custom.versions.v1}/publication-versions/{id}/my-affiliations method: PUT cors: true @@ -520,4 +535,3 @@ functions: path: ${self:custom.versions.v1}/publications/{id}/topics method: PUT cors: true - diff --git a/api/src/components/affiliations/__tests__/updateAffiliations.test.ts b/api/src/components/affiliations/__tests__/updateAffiliations.test.ts index c310c1ec6..c1b2c679c 100644 --- a/api/src/components/affiliations/__tests__/updateAffiliations.test.ts +++ b/api/src/components/affiliations/__tests__/updateAffiliations.test.ts @@ -69,7 +69,7 @@ describe('update coAuthor affiliations per publication', () => { test('Corresponding author can add an affiliation to their DRAFT publication', async () => { const updateAffiliationsResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/my-affiliations') + .put('/publication-versions/publication-problem-draft-v1/my-affiliations') .query({ apiKey: '000000005' }) .send({ affiliations: orcidTestAffiliations, @@ -82,7 +82,7 @@ describe('update coAuthor affiliations per publication', () => { test('Corresponding author needs to add affiliations if not independent before locking the publication', async () => { const updateAffiliationsResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/my-affiliations') + .put('/publication-versions/publication-problem-draft-v1/my-affiliations') .query({ apiKey: '000000005' }) .send({ affiliations: [], @@ -92,7 +92,7 @@ describe('update coAuthor affiliations per publication', () => { expect(updateAffiliationsResponse.status).toEqual(200); const updateStatusResponse = await testUtils.agent - .put('/publications/publication-problem-draft/status/LOCKED') + .put('/publication-versions/publication-problem-draft-v1/status/LOCKED') .query({ apiKey: '000000005' }) .send(); @@ -104,7 +104,7 @@ describe('update coAuthor affiliations per publication', () => { test('Author needs to fill out affiliations if the publication is LOCKED', async () => { const updateAffiliationsResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-locked-v1/my-affiliations') + .put('/publication-versions/publication-problem-locked-v1/my-affiliations') .query({ apiKey: '000000005' }) .send({ affiliations: [], @@ -117,7 +117,7 @@ describe('update coAuthor affiliations per publication', () => { test('User cannot add affiliations if they are not part of the publication', async () => { const updateAffiliationsResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/my-affiliations') + .put('/publication-versions/publication-problem-draft-v1/my-affiliations') .query({ apiKey: '123456789' }) .send({ affiliations: [], @@ -132,7 +132,7 @@ describe('update coAuthor affiliations per publication', () => { test('User cannot add affiliations if the publication is LIVE', async () => { const updateAffiliationsResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-live-v1/my-affiliations') + .put('/publication-versions/publication-problem-live-v1/my-affiliations') .query({ apiKey: '123456789' }) .send({ affiliations: [], @@ -145,7 +145,7 @@ describe('update coAuthor affiliations per publication', () => { test('Only corresponding author can update his affiliations while the publication is DRAFT', async () => { const updateAffiliationsResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/my-affiliations') + .put('/publication-versions/publication-problem-draft-v1/my-affiliations') .query({ apiKey: '000000005' }) .send({ affiliations: orcidTestAffiliations, @@ -156,7 +156,7 @@ describe('update coAuthor affiliations per publication', () => { expect(updateAffiliationsResponse.body.message).toEqual('Successfully updated affiliations.'); const updateAffiliationsResponse2 = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/my-affiliations') + .put('/publication-versions/publication-problem-draft-v1/my-affiliations') .query({ apiKey: '000000006' }) .send({ affiliations: orcidTestAffiliations, @@ -171,7 +171,7 @@ describe('update coAuthor affiliations per publication', () => { test('Cannot add duplicate affiliations', async () => { const updateAffiliationsResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/my-affiliations') + .put('/publication-versions/publication-problem-draft-v1/my-affiliations') .query({ apiKey: '000000005' }) .send({ affiliations: orcidTestAffiliations.concat(orcidTestAffiliations), diff --git a/api/src/components/affiliations/controller.ts b/api/src/components/affiliations/controller.ts index cea466723..d14a3d16c 100644 --- a/api/src/components/affiliations/controller.ts +++ b/api/src/components/affiliations/controller.ts @@ -16,7 +16,7 @@ export const updateAffiliations = async ( const affiliations = event.body.affiliations; const publicationVersionId = event.pathParameters.id; // Get publication version - const version = await publicationVersionService.get(publicationVersionId); + const version = await publicationVersionService.getById(publicationVersionId); // Check that the version exists if (!version) { diff --git a/api/src/components/coauthor/__tests__/createCoAuthor.test.ts b/api/src/components/coauthor/__tests__/createCoAuthor.test.ts index f10377fb0..87418e7b3 100644 --- a/api/src/components/coauthor/__tests__/createCoAuthor.test.ts +++ b/api/src/components/coauthor/__tests__/createCoAuthor.test.ts @@ -9,7 +9,7 @@ describe('create coauthor', () => { test('Update co-authors for a specific publication version', async () => { const coauthor = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/coauthors') + .put('/publication-versions/publication-problem-draft-v1/coauthors') .query({ apiKey: '000000005' }) .send([ { @@ -34,7 +34,7 @@ describe('create coauthor', () => { test('Cannot create a co-author with duplicate email', async () => { const coauthor = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/coauthors') + .put('/publication-versions/publication-problem-draft-v1/coauthors') .query({ apiKey: '000000005' }) .send([ { @@ -75,7 +75,7 @@ describe('create coauthor', () => { test('Cannot create a co-author record if the user is not the author of the publication version', async () => { const coauthor = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/coauthors') + .put('/publication-versions/publication-problem-draft-v1/coauthors') .query({ apiKey: '987654321' }) .send([ { @@ -93,7 +93,7 @@ describe('create coauthor', () => { test('Cannot create a co-author record on a publication version that does not exist', async () => { const coauthor = await testUtils.agent - .put('/publicationVersions/non-existent-publication-version/coauthors') + .put('/publication-versions/non-existent-publication-version/coauthors') .query({ apiKey: '123456789' }) .send([ { @@ -111,7 +111,7 @@ describe('create coauthor', () => { test('Cannot create a co-author record on a publication version that is live', async () => { const coauthor = await testUtils.agent - .put('/publicationVersions/publication-problem-live-v1/coauthors') + .put('/publication-versions/publication-problem-live-v1/coauthors') .query({ apiKey: '123456789' }) .send([ { diff --git a/api/src/components/coauthor/__tests__/deleteCoAuthor.test.ts b/api/src/components/coauthor/__tests__/deleteCoAuthor.test.ts index a8bb2b713..0413b4575 100644 --- a/api/src/components/coauthor/__tests__/deleteCoAuthor.test.ts +++ b/api/src/components/coauthor/__tests__/deleteCoAuthor.test.ts @@ -8,7 +8,7 @@ describe('Delete co-author', () => { test('Delete a co-author', async () => { const deleteCoAuthor = await testUtils.agent - .delete('/publicationVersions/publication-problem-draft-v1/coauthors/coauthor-test-user-6-problem-draft') + .delete('/publication-versions/publication-problem-draft-v1/coauthors/coauthor-test-user-6-problem-draft') .query({ apiKey: '000000005' }); expect(deleteCoAuthor.status).toEqual(200); @@ -16,7 +16,7 @@ describe('Delete co-author', () => { test('Cannot Delete a co-author without a valid id/coauthor has not been added to this publication version', async () => { const deleteCoAuthor = await testUtils.agent - .delete('/publicationVersions/publication-problem-draft-v1/coauthors/invalid-id') + .delete('/publication-versions/publication-problem-draft-v1/coauthors/invalid-id') .query({ apiKey: '000000005' }); expect(deleteCoAuthor.status).toEqual(404); @@ -24,7 +24,7 @@ describe('Delete co-author', () => { test('Cannot Delete a co-author record if the user is not the author of a publication version', async () => { const deleteCoAuthor = await testUtils.agent - .delete('/publicationVersions/publication-problem-draft-v1/coauthors/coauthor-test-user-5-problem-draft') + .delete('/publication-versions/publication-problem-draft-v1/coauthors/coauthor-test-user-5-problem-draft') .query({ apiKey: '987654321' }); expect(deleteCoAuthor.status).toEqual(403); @@ -32,7 +32,7 @@ describe('Delete co-author', () => { test('Cannot Delete a co-author record if the publication version is live', async () => { const deleteCoAuthor = await testUtils.agent - .delete('/publicationVersions/publication-problem-draft-v1/coauthors/co-author-test-user-6-problem-live') + .delete('/publication-versions/publication-problem-draft-v1/coauthors/co-author-test-user-6-problem-live') .query({ apiKey: '000000005' }); expect(deleteCoAuthor.status).toEqual(404); @@ -40,7 +40,7 @@ describe('Delete co-author', () => { test('Cannot Delete a co-author record on a publication version that does not exist', async () => { const deleteCoAuthor = await testUtils.agent - .delete('/publicationVersions/non-existent-publication-v1/coauthors/coauthor-test-user-5-problem-draft') + .delete('/publication-versions/non-existent-publication-v1/coauthors/coauthor-test-user-5-problem-draft') .query({ apiKey: '123456789' }); expect(deleteCoAuthor.status).toEqual(404); diff --git a/api/src/components/coauthor/__tests__/linkCoAuthor.test.ts b/api/src/components/coauthor/__tests__/linkCoAuthor.test.ts index cabbf2ed8..b2cd0920b 100644 --- a/api/src/components/coauthor/__tests__/linkCoAuthor.test.ts +++ b/api/src/components/coauthor/__tests__/linkCoAuthor.test.ts @@ -9,7 +9,7 @@ describe('Link co-author', () => { test('Link a co-author to a publication version (allow)', async () => { const link = await testUtils.agent - .patch('/publicationVersions/publication-hypothesis-draft-v1/link-coauthor') + .patch('/publication-versions/publication-hypothesis-draft-v1/link-coauthor') .query({ apiKey: '000000007' }) .send({ email: 'test-user-7@jisc.ac.uk', @@ -27,7 +27,7 @@ describe('Link co-author', () => { test('Link a co-author to a publication version (do not allow) with authentication', async () => { const link = await testUtils.agent - .patch('/publicationVersions/publication-problem-draft-v1/link-coauthor') + .patch('/publication-versions/publication-problem-draft-v1/link-coauthor') .query({ apiKey: '987654321' }) .send({ email: 'test-user-7@jisc.ac.uk', @@ -45,7 +45,7 @@ describe('Link co-author', () => { test('Link a co-author to a publication version (do not allow) without authentication', async () => { const link = await testUtils.agent - .patch('/publicationVersions/publication-problem-draft-v1/link-coauthor') + .patch('/publication-versions/publication-problem-draft-v1/link-coauthor') .query({ apiKey: '987654321' }) .send({ email: 'test-user-7@jisc.ac.uk', @@ -58,7 +58,7 @@ describe('Link co-author', () => { test('Cannot link as co-author if you are the creator', async () => { const link = await testUtils.agent - .patch('/publicationVersions/publication-problem-draft-v1/link-coauthor') + .patch('/publication-versions/publication-problem-draft-v1/link-coauthor') .query({ apiKey: '000000005' }) .send({ email: 'test-user-7@jisc.ac.uk', @@ -71,7 +71,7 @@ describe('Link co-author', () => { test('Cannot link co-author if user has already been linked as another co-author', async () => { const link = await testUtils.agent - .patch('/publicationVersions/publication-problem-draft-v1/link-coauthor') + .patch('/publication-versions/publication-problem-draft-v1/link-coauthor') .query({ apiKey: '000000006' }) .send({ email: 'test-user-6@jisc.ac.uk', @@ -84,7 +84,7 @@ describe('Link co-author', () => { test('Cannot override co-authorship', async () => { const link = await testUtils.agent - .patch('/publicationVersions/publication-problem-draft-v1/link-coauthor') + .patch('/publication-versions/publication-problem-draft-v1/link-coauthor') .query({ apiKey: '987654321' }) .send({ email: 'test-user-6@jisc.ac.uk', @@ -99,7 +99,7 @@ describe('Link co-author', () => { test('Cannot link co-author with a different email address', async () => { // trying to accept invitation with a user which is not a co-author const response = await testUtils.agent - .patch('/publicationVersions/publication-problem-draft-v1/link-coauthor') + .patch('/publication-versions/publication-problem-draft-v1/link-coauthor') .query({ apiKey: '000000004' }) .send({ email: 'test-user-7@jisc.ac.uk', @@ -112,7 +112,7 @@ describe('Link co-author', () => { // trying to accept invitation with a different co-author account const response2 = await testUtils.agent - .patch('/publicationVersions/publication-problem-draft-v1/link-coauthor') + .patch('/publication-versions/publication-problem-draft-v1/link-coauthor') .query({ apiKey: '000000008' }) .send({ email: 'test-user-7@jisc.ac.uk', diff --git a/api/src/components/coauthor/__tests__/requestApproval.test.ts b/api/src/components/coauthor/__tests__/requestApproval.test.ts index 0c12b5ae6..6bed0a8a4 100644 --- a/api/src/components/coauthor/__tests__/requestApproval.test.ts +++ b/api/src/components/coauthor/__tests__/requestApproval.test.ts @@ -8,13 +8,13 @@ describe('Request co-authors approvals', () => { test('Can request approvals only if the publication version is DRAFT or LOCKED', async () => { const draftPublicationVersionResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/coauthors/request-approval') + .put('/publication-versions/publication-problem-draft-v1/coauthors/request-approval') .query({ apiKey: '000000005' }); expect(draftPublicationVersionResponse.status).toEqual(200); const lockedPublicationVersionResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-locked-v1/coauthors/request-approval') + .put('/publication-versions/publication-problem-locked-v1/coauthors/request-approval') .query({ apiKey: '000000005' }); expect(lockedPublicationVersionResponse.status).toEqual(200); @@ -22,7 +22,7 @@ describe('Request co-authors approvals', () => { test('Cannot request approvals for a LIVE publication version', async () => { const livePublicationVersionResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-live-v1/coauthors/request-approval') + .put('/publication-versions/publication-problem-live-v1/coauthors/request-approval') .query({ apiKey: '123456789' }); expect(livePublicationVersionResponse.status).toEqual(403); @@ -30,7 +30,7 @@ describe('Request co-authors approvals', () => { test('Cannot request approvals if user is not the creator', async () => { const draftPublicationVersionResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/coauthors/request-approval') + .put('/publication-versions/publication-problem-draft-v1/coauthors/request-approval') .query({ apiKey: '000000006' }); expect(draftPublicationVersionResponse.status).toEqual(403); @@ -38,7 +38,7 @@ describe('Request co-authors approvals', () => { test('Cannot request approvals if publication has no-coauthors', async () => { const draftPublicationVersionResponse = await testUtils.agent - .put('/publicationVersions/publication-2-v1/coauthors/request-approval') + .put('/publication-versions/publication-2-v1/coauthors/request-approval') .query({ apiKey: '987654321' }); expect(draftPublicationVersionResponse.status).toEqual(403); diff --git a/api/src/components/coauthor/__tests__/sendApprovalReminder.test.ts b/api/src/components/coauthor/__tests__/sendApprovalReminder.test.ts index 8e9447348..5209e7009 100644 --- a/api/src/components/coauthor/__tests__/sendApprovalReminder.test.ts +++ b/api/src/components/coauthor/__tests__/sendApprovalReminder.test.ts @@ -10,7 +10,7 @@ describe('Request co-authors approvals', () => { // test LIVE const livePublicationVersionResponse = await testUtils.agent .post( - '/publicationVersions/publication-problem-live-v1/coauthors/coauthor-test-user-6-problem-live/approval-reminder' + '/publication-versions/publication-problem-live-v1/coauthors/coauthor-test-user-6-problem-live/approval-reminder' ) .query({ apiKey: '123456789' }); @@ -22,7 +22,7 @@ describe('Request co-authors approvals', () => { // test DRAFT const draftPublicationVersionResponse = await testUtils.agent .post( - '/publicationVersions/publication-problem-draft-v1/coauthors/coauthor-test-user-7-problem-draft/approval-reminder' + '/publication-versions/publication-problem-draft-v1/coauthors/coauthor-test-user-7-problem-draft/approval-reminder' ) .query({ apiKey: '000000005' }); @@ -34,7 +34,7 @@ describe('Request co-authors approvals', () => { // test LOCKED const lockedPublicationVersionResponse = await testUtils.agent .post( - '/publicationVersions/publication-problem-locked-v1/coauthors/coauthor-test-user-7-problem-locked/approval-reminder' + '/publication-versions/publication-problem-locked-v1/coauthors/coauthor-test-user-7-problem-locked/approval-reminder' ) .query({ apiKey: '000000005' }); @@ -45,7 +45,7 @@ describe('Request co-authors approvals', () => { test('Cannot send approval reminder if a co-author already approved', async () => { const response = await testUtils.agent .post( - '/publicationVersions/locked-publication-problem-confirmed-co-authors-v1/coauthors/test-user-2/approval-reminder' + '/publication-versions/locked-publication-problem-confirmed-co-authors-v1/coauthors/test-user-2/approval-reminder' ) .query({ apiKey: '123456789' }); @@ -56,7 +56,7 @@ describe('Request co-authors approvals', () => { test('Cannot send approval reminder to a user which is not a co-author', async () => { const response = await testUtils.agent .post( - '/publicationVersions/locked-publication-problem-confirmed-co-authors-v1/coauthors/test-user-22/approval-reminder' + '/publication-versions/locked-publication-problem-confirmed-co-authors-v1/coauthors/test-user-22/approval-reminder' ) .query({ apiKey: '123456789' }); @@ -67,7 +67,7 @@ describe('Request co-authors approvals', () => { test('Cannot send approval reminder to the same co-author twice', async () => { const response1 = await testUtils.agent .post( - '/publicationVersions/publication-problem-locked-v1/coauthors/coauthor-test-user-7-problem-locked/approval-reminder' + '/publication-versions/publication-problem-locked-v1/coauthors/coauthor-test-user-7-problem-locked/approval-reminder' ) .query({ apiKey: '000000005' }); @@ -76,7 +76,7 @@ describe('Request co-authors approvals', () => { const response2 = await testUtils.agent .post( - '/publicationVersions/publication-problem-locked-v1/coauthors/coauthor-test-user-7-problem-locked/approval-reminder' + '/publication-versions/publication-problem-locked-v1/coauthors/coauthor-test-user-7-problem-locked/approval-reminder' ) .query({ apiKey: '000000005' }); diff --git a/api/src/components/coauthor/__tests__/updateCoAuthor.test.ts b/api/src/components/coauthor/__tests__/updateCoAuthor.test.ts index fc8867a3d..63113a56b 100644 --- a/api/src/components/coauthor/__tests__/updateCoAuthor.test.ts +++ b/api/src/components/coauthor/__tests__/updateCoAuthor.test.ts @@ -8,7 +8,7 @@ describe('Update co-author status', () => { test('Co-author updates their status to true', async () => { const coAuthor = await testUtils.agent - .patch('/publicationVersions/publication-problem-locked-v1/coauthor-confirmation') + .patch('/publication-versions/publication-problem-locked-v1/coauthor-confirmation') .query({ apiKey: '000000006' }) .send({ confirm: true @@ -19,7 +19,7 @@ describe('Update co-author status', () => { test('Co-author updates their status to false', async () => { const coAuthor = await testUtils.agent - .patch('/publicationVersions/publication-problem-locked-v1/coauthor-confirmation') + .patch('/publication-versions/publication-problem-locked-v1/coauthor-confirmation') .query({ apiKey: '000000006' }) .send({ confirm: false @@ -30,7 +30,7 @@ describe('Update co-author status', () => { test('Cannot update co-author if not a co-author (1)', async () => { const coAuthor = await testUtils.agent - .patch('/publicationVersions/publication-problem-locked-v1/coauthor-confirmation') + .patch('/publication-versions/publication-problem-locked-v1/coauthor-confirmation') .query({ apiKey: '123456789' }) .send({ confirm: true @@ -41,7 +41,7 @@ describe('Update co-author status', () => { test('Cannot update co-author if not a co-author (2)', async () => { const coAuthor = await testUtils.agent - .patch('/publicationVersions/publication-problem-locked-v1/coauthor-confirmation') + .patch('/publication-versions/publication-problem-locked-v1/coauthor-confirmation') .query({ apiKey: '987654321' }) .send({ confirm: true diff --git a/api/src/components/coauthor/controller.ts b/api/src/components/coauthor/controller.ts index 614d56df6..85e56c110 100644 --- a/api/src/components/coauthor/controller.ts +++ b/api/src/components/coauthor/controller.ts @@ -2,16 +2,13 @@ import * as coAuthorService from 'coauthor/service'; import * as I from 'interface'; import * as email from 'email'; import * as response from 'lib/response'; -import * as publicationService from 'publication/service'; import * as publicationVersionService from 'publicationVersion/service'; export const get = async ( event: I.AuthenticatedAPIRequest ): Promise => { try { - const versionId = event.pathParameters.id; - - const version = await publicationVersionService.get(versionId); + const version = await publicationVersionService.getById(event.pathParameters.id); if (!version) { return response.json(404, { @@ -50,8 +47,7 @@ export const updateAll = async ( event: I.AuthenticatedAPIRequest ): Promise => { try { - const versionId = event.pathParameters.id; - const version = await publicationVersionService.get(versionId); + const version = await publicationVersionService.getById(event.pathParameters.id); // Does the publication version exist? if (!version) { @@ -130,7 +126,7 @@ export const remove = async ( event: I.AuthenticatedAPIRequest ): Promise => { try { - const version = await publicationVersionService.get(event.pathParameters.id); + const version = await publicationVersionService.getById(event.pathParameters.id); // Does the publication version exist? if (!version) { @@ -184,7 +180,7 @@ export const link = async ( event: I.OptionalAuthenticatedAPIRequest ): Promise => { try { - const version = await publicationVersionService.get(event.pathParameters.id); + const version = await publicationVersionService.getById(event.pathParameters.id); if (!version) { return response.json(404, { @@ -296,7 +292,7 @@ export const updateConfirmation = async ( event: I.AuthenticatedAPIRequest ): Promise => { try { - const version = await publicationVersionService.get(event.pathParameters.id); + const version = await publicationVersionService.getById(event.pathParameters.id); // Does the publication version exist? if (!version) { @@ -375,7 +371,7 @@ export const requestApproval = async ( ): Promise => { try { const versionId = event.pathParameters.id; - const version = await publicationVersionService.get(versionId); + const version = await publicationVersionService.getById(versionId); if (!version) { return response.json(404, { message: 'Publication version not found' }); @@ -398,35 +394,31 @@ export const requestApproval = async ( } if (version.currentStatus === 'DRAFT') { - const publication = await publicationService.getWithVersion(version.versionOf, version.versionNumber); + const isReadyToRequestApprovals = await publicationVersionService.checkIsReadyToRequestApprovals(version); - if (publication) { - if (!publicationService.isReadyToRequestApproval(publication)) { - return response.json(403, { - message: - 'Approval emails cannot be sent because the publication is not ready to be LOCKED. Make sure all fields are filled in.' - }); - } + if (!isReadyToRequestApprovals) { + return response.json(403, { + message: + 'Approval emails cannot be sent because the publication is not ready to be LOCKED. Make sure all fields are filled in.' + }); + } + + // check if this version was LOCKED before + if (version.publicationStatus.some(({ status }) => status === 'LOCKED')) { + // notify linked co-authors about changes + const linkedCoAuthors = version.coAuthors.filter( + (author) => author.linkedUser && author.linkedUser !== version.createdBy + ); - // check if this version was LOCKED before - if (version.publicationStatus.some(({ status }) => status === 'LOCKED')) { - // notify linked co-authors about changes - const linkedCoAuthors = version.coAuthors.filter( - (author) => author.linkedUser && author.linkedUser !== version.createdBy - ); - - for (const linkedCoAuthor of linkedCoAuthors) { - await email.notifyCoAuthorsAboutChanges({ - coAuthor: { email: linkedCoAuthor.email }, - publication: { - title: version.title || '', - url: `${process.env.BASE_URL}/publications/${publication.id}` - } - }); - } + for (const linkedCoAuthor of linkedCoAuthors) { + await email.notifyCoAuthorsAboutChanges({ + coAuthor: { email: linkedCoAuthor.email }, + publication: { + title: version.title || '', + url: `${process.env.BASE_URL}/publications/${version.versionOf}` + } + }); } - } else { - throw Error('Could not get details of publication'); } } @@ -463,7 +455,7 @@ export const sendApprovalReminder = async ( ): Promise => { const { coauthor, id } = event.pathParameters; - const version = await publicationVersionService.get(id); + const version = await publicationVersionService.getById(id); const author = await coAuthorService.get(coauthor); if (!version) { diff --git a/api/src/components/flag/__tests__/createFlag.test.ts b/api/src/components/flag/__tests__/createFlag.test.ts index efab652c0..d74881f39 100644 --- a/api/src/components/flag/__tests__/createFlag.test.ts +++ b/api/src/components/flag/__tests__/createFlag.test.ts @@ -8,7 +8,7 @@ describe('Create flags on publications', () => { test('User can create a valid flag on LIVE publication they did not create', async () => { const createFlag = await testUtils.agent - .post('/publications/publication-interpretation-live/flag') + .post('/publications/publication-interpretation-live/flags') .query({ apiKey: '987654321' }) @@ -22,7 +22,7 @@ describe('Create flags on publications', () => { test('User cannot create a valid flag on LIVE publication they created', async () => { const createFlag = await testUtils.agent - .post('/publications/publication-interpretation-live/flag') + .post('/publications/publication-interpretation-live/flags') .query({ apiKey: '123456789' }) @@ -36,7 +36,7 @@ describe('Create flags on publications', () => { test('User cannot create a invalid flag on LIVE publication they did not create', async () => { const createFlag = await testUtils.agent - .post('/publications/publication-interpretation-live/flag') + .post('/publications/publication-interpretation-live/flags') .query({ apiKey: '987654321' }) @@ -50,7 +50,7 @@ describe('Create flags on publications', () => { test('User cannot create a duplicate flag for an unresolved flag', async () => { const createFlag = await testUtils.agent - .post('/publications/publication-interpretation-live/flag') + .post('/publications/publication-interpretation-live/flags') .query({ apiKey: '987654321' }) @@ -62,7 +62,7 @@ describe('Create flags on publications', () => { expect(createFlag.status).toEqual(200); const createFlagAttempt2 = await testUtils.agent - .post('/publications/publication-interpretation-live/flag') + .post('/publications/publication-interpretation-live/flags') .query({ apiKey: '987654321' }) @@ -76,7 +76,7 @@ describe('Create flags on publications', () => { test('Cannot create a valid flag for a publication that is in DRAFT', async () => { const createFlag = await testUtils.agent - .post('/publications/publication-interpretation-draft/flag') + .post('/publications/publication-interpretation-draft/flags') .query({ apiKey: '987654321' }) @@ -90,7 +90,7 @@ describe('Create flags on publications', () => { test('User can create 2 differente flags for the same publication that they did not create', async () => { const createFlag = await testUtils.agent - .post('/publications/publication-interpretation-live/flag') + .post('/publications/publication-interpretation-live/flags') .query({ apiKey: '987654321' }) @@ -102,7 +102,7 @@ describe('Create flags on publications', () => { expect(createFlag.status).toEqual(200); const createFlagAttempt2 = await testUtils.agent - .post('/publications/publication-interpretation-live/flag') + .post('/publications/publication-interpretation-live/flags') .query({ apiKey: '987654321' }) diff --git a/api/src/components/flag/__tests__/createFlagComment.test.ts b/api/src/components/flag/__tests__/createFlagComment.test.ts index 141cb041e..cdd39a9a7 100644 --- a/api/src/components/flag/__tests__/createFlagComment.test.ts +++ b/api/src/components/flag/__tests__/createFlagComment.test.ts @@ -8,7 +8,7 @@ describe('Create flags comments on a flag', () => { test('User who created the flag can leave comments', async () => { const createFlagComment = await testUtils.agent - .post('/flag/publication-problem-live-flag/comment') + .post('/flags/publication-problem-live-flag/comment') .query({ apiKey: '987654321' }) @@ -21,7 +21,7 @@ describe('Create flags comments on a flag', () => { test('Owner of the publication can leave comments', async () => { const createFlagComment = await testUtils.agent - .post('/flag/publication-problem-live-flag/comment') + .post('/flags/publication-problem-live-flag/comment') .query({ apiKey: '123456789' }) @@ -33,7 +33,7 @@ describe('Create flags comments on a flag', () => { test('You cannot leave a comment on an resolved flag', async () => { const createFlagComment = await testUtils.agent - .post('/flag/publication-hypothesis-live/comment') + .post('/flags/publication-hypothesis-live/comment') .query({ apiKey: '123456789' }) @@ -46,7 +46,7 @@ describe('Create flags comments on a flag', () => { test('You cannot leave a comment on an un-flagged publication', async () => { const createFlagComment = await testUtils.agent - .post('/flag/publication-analysis-live/comment') + .post('/flags/publication-analysis-live/comment') .query({ apiKey: '123456789' }) @@ -59,7 +59,7 @@ describe('Create flags comments on a flag', () => { test('You can only leave a comment if you are either the author of the publication or the flagger', async () => { const createFlagComment = await testUtils.agent - .post('/flag/publication-problem-live-flag/comment') + .post('/flags/publication-problem-live-flag/comment') .query({ apiKey: '000000003' }) @@ -72,7 +72,7 @@ describe('Create flags comments on a flag', () => { test('The body of the request is invalid', async () => { const createFlagComment = await testUtils.agent - .post('/flag/publication-problem-live-flag/comment') + .post('/flags/publication-problem-live-flag/comment') .query({ apiKey: '123456789' }) diff --git a/api/src/components/flag/__tests__/resolveFlag.test.ts b/api/src/components/flag/__tests__/resolveFlag.test.ts index 4515a9c83..a41866c47 100644 --- a/api/src/components/flag/__tests__/resolveFlag.test.ts +++ b/api/src/components/flag/__tests__/resolveFlag.test.ts @@ -7,7 +7,7 @@ describe('Resolve a flag', () => { }); test('The flagger can resolve the flag', async () => { - const resolveFlag = await testUtils.agent.post('/flag/publication-problem-live-flag/resolve').query({ + const resolveFlag = await testUtils.agent.post('/flags/publication-problem-live-flag/resolve').query({ apiKey: '987654321' }); @@ -15,7 +15,7 @@ describe('Resolve a flag', () => { }); test('Only the flagger or super user can resolve the flag', async () => { - const resolveFlag = await testUtils.agent.post('/flag/publication-problem-live-flag/resolve').query({ + const resolveFlag = await testUtils.agent.post('/flags/publication-problem-live-flag/resolve').query({ apiKey: '123456789' }); @@ -23,7 +23,7 @@ describe('Resolve a flag', () => { }); test('A super user can resolve the flag', async () => { - const resolveFlag = await testUtils.agent.post('/flag/publication-problem-live-flag/resolve').query({ + const resolveFlag = await testUtils.agent.post('/flags/publication-problem-live-flag/resolve').query({ apiKey: '000000004' }); @@ -31,7 +31,7 @@ describe('Resolve a flag', () => { }); test('An unrelated user cannot resolve a flag', async () => { - const resolveFlag = await testUtils.agent.post('/flag/publication-problem-live-flag/resolve').query({ + const resolveFlag = await testUtils.agent.post('/flags/publication-problem-live-flag/resolve').query({ apiKey: '000000003' }); @@ -39,7 +39,7 @@ describe('Resolve a flag', () => { }); test('You cannot resolve a flag that has already been resolved', async () => { - const resolveFlag = await testUtils.agent.post('/flag/publication-hypothesis-live-flag/resolve').query({ + const resolveFlag = await testUtils.agent.post('/flags/publication-hypothesis-live-flag/resolve').query({ apiKey: '987654321' }); @@ -47,7 +47,7 @@ describe('Resolve a flag', () => { }); test('You can only resolve a flag that exists', async () => { - const resolveFlag = await testUtils.agent.post('/flag/publication-does-not-exist-flag/resolve').query({ + const resolveFlag = await testUtils.agent.post('/flags/publication-does-not-exist-flag/resolve').query({ apiKey: '987654321' }); diff --git a/api/src/components/funder/__tests__/createFunder.test.ts b/api/src/components/funder/__tests__/createFunder.test.ts index 19da39d81..dad2c8018 100644 --- a/api/src/components/funder/__tests__/createFunder.test.ts +++ b/api/src/components/funder/__tests__/createFunder.test.ts @@ -1,14 +1,14 @@ import * as testUtils from 'lib/testUtils'; describe('create a funder', () => { - beforeEach(async () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); - test('User can add a funder to their DRAFT publication', async () => { + test('User can add a funder to their DRAFT publication version', async () => { const funder = await testUtils.agent - .post('/publications/publication-problem-draft/funders') + .post('/publication-versions/publication-problem-draft-v1/funders') .query({ apiKey: '000000005' }) .send({ name: 'Example name', @@ -19,9 +19,9 @@ describe('create a funder', () => { expect(funder.status).toEqual(200); }); - test('User cannot add a funder to their LIVE publication', async () => { + test('User cannot add a funder to their LIVE publication version', async () => { const funder = await testUtils.agent - .post('/publications/publication-problem-live/funders') + .post('/publication-versions/publication-problem-live-v1/funders') .query({ apiKey: '123456789' }) .send({ name: 'Example name', @@ -32,9 +32,9 @@ describe('create a funder', () => { expect(funder.status).toEqual(403); }); - test('User cannot add a funder to another DRAFT publication', async () => { + test('User cannot add a funder to another DRAFT publication version', async () => { const funder = await testUtils.agent - .post('/publications/publication-problem-draft/funders') + .post('/publication-versions/publication-problem-draft-v1/funders') .query({ apiKey: '987654321' }) .send({ name: 'Example name', @@ -45,9 +45,9 @@ describe('create a funder', () => { expect(funder.status).toEqual(403); }); - test('User cannot add a funder to another LIVE publication', async () => { + test('User cannot add a funder to another LIVE publication version', async () => { const funder = await testUtils.agent - .post('/publications/publication-problem-live/funders') + .post('/publication-versions/publication-problem-live-v1/funders') .query({ apiKey: '987654321' }) .send({ name: 'Example name', @@ -60,7 +60,7 @@ describe('create a funder', () => { }); test('User must send correct information to create a funder (no name)', async () => { const funder = await testUtils.agent - .post('/publications/publication-problem-draft/funders') + .post('/publication-versions/publication-problem-draft-v1/funders') .query({ apiKey: '000000005' }) .send({ city: 'Example city', @@ -72,7 +72,7 @@ describe('create a funder', () => { }); test('User must send correct information to create a funder (no city)', async () => { const funder = await testUtils.agent - .post('/publications/publication-problem-draft/funders') + .post('/publication-versions/publication-problem-draft-v1/funders') .query({ apiKey: '000000005' }) .send({ name: 'Example name', @@ -84,7 +84,7 @@ describe('create a funder', () => { }); test('User must send correct information to create a funder (no country)', async () => { const funder = await testUtils.agent - .post('/publications/publication-problem-draft/funders') + .post('/publication-versions/publication-problem-draft-v1/funders') .query({ apiKey: '000000005' }) .send({ name: 'Example name', @@ -96,7 +96,7 @@ describe('create a funder', () => { }); test('User must send correct information to create a funder (no link)', async () => { const funder = await testUtils.agent - .post('/publications/publication-problem-draft/funders') + .post('/publication-versions/publication-problem-draft-v1/funders') .query({ apiKey: '000000005' }) .send({ name: 'Example name', diff --git a/api/src/components/funder/__tests__/deleteFunder.test.ts b/api/src/components/funder/__tests__/deleteFunder.test.ts index 2a2fbd4e4..680a3b8ee 100644 --- a/api/src/components/funder/__tests__/deleteFunder.test.ts +++ b/api/src/components/funder/__tests__/deleteFunder.test.ts @@ -6,17 +6,17 @@ describe('delete a funder', () => { await testUtils.testSeed(); }); - test('User can delete a funder from a DRAFT publication', async () => { + test('User can delete a funder from a DRAFT publication version', async () => { const funder = await testUtils.agent - .delete('/publications/publication-problem-draft/funders/publication-problem-draft-funder') + .delete('/publication-versions/publication-problem-draft-v1/funders/publication-problem-draft-funder') .query({ apiKey: '000000005' }); expect(funder.status).toEqual(200); }); - test('User cannot delete a funder from a DRAFT publication they are not the owner of', async () => { + test('User cannot delete a funder from a DRAFT publication version they are not the owner of', async () => { const funder = await testUtils.agent - .delete('/publications/publication-problem-draft/funders/publication-problem-draft-funder') + .delete('/publication-versions/publication-problem-draft-v1/funders/publication-problem-draft-funder') .query({ apiKey: '987654321' }); expect(funder.status).toEqual(403); @@ -24,7 +24,7 @@ describe('delete a funder', () => { test('User cannot delete a funder from a LIVE publication', async () => { const funder = await testUtils.agent - .delete('/publications/publication-problem-live/funders/publication-problem-live-funder') + .delete('/publication-versions/publication-problem-live-v1/funders/publication-problem-live-funder') .query({ apiKey: '123456789' }); expect(funder.status).toEqual(403); diff --git a/api/src/components/funder/controller.ts b/api/src/components/funder/controller.ts index 5f7d5f6ba..34c2a73ae 100644 --- a/api/src/components/funder/controller.ts +++ b/api/src/components/funder/controller.ts @@ -1,37 +1,35 @@ import * as response from 'lib/response'; import * as funderService from 'funder/service'; -import * as publicationService from 'publication/service'; +import * as publicationVersionService from 'publicationVersion/service'; import * as I from 'interface'; export const create = async ( event: I.AuthenticatedAPIRequest ): Promise => { try { - const publication = await publicationService.getWithVersion(event.pathParameters.id); + const publicationVersion = await publicationVersionService.getById(event.pathParameters.id); //check that the publication exists - if (!publication) { + if (!publicationVersion) { return response.json(404, { - message: 'This publication does not exist.' + message: 'This publication version does not exist.' }); } - const currentVersion = publication.versions[0]; - //check that the publication is live - if (currentVersion.currentStatus !== 'DRAFT') { + if (publicationVersion.currentStatus !== 'DRAFT') { return response.json(403, { message: 'You can only add funding to a draft publication.' }); } - if (event.user.id !== currentVersion.user.id) { + if (event.user.id !== publicationVersion.user.id) { return response.json(403, { message: 'You do not have permissions to add a funder.' }); } - const funder = await funderService.create(currentVersion.id, event.body); + const funder = await funderService.create(publicationVersion.id, event.body); return response.json(200, funder); } catch (err) { @@ -45,31 +43,29 @@ export const destroy = async ( event: I.AuthenticatedAPIRequest ): Promise => { try { - const publication = await publicationService.getWithVersion(event.pathParameters.id); + const publicationVersion = await publicationVersionService.getById(event.pathParameters.id); - //check that the publication exists - if (!publication) { + //check that the publication version exists + if (!publicationVersion) { return response.json(404, { - message: 'This publication does not exist.' + message: 'This publication version does not exist.' }); } - const currentVersion = publication?.versions[0]; - //check that the publication is live - if (currentVersion.currentStatus !== 'DRAFT') { + if (publicationVersion.currentStatus !== 'DRAFT') { return response.json(403, { message: 'You cannot delete funding from a publication that is not a draft.' }); } - if (event.user.id !== currentVersion.user.id) { + if (event.user.id !== publicationVersion.user.id) { return response.json(403, { message: 'You do not have permissions to delete a funder.' }); } - const funder = await funderService.destroy(currentVersion.id, event.pathParameters.funder); + const funder = await funderService.destroy(publicationVersion.id, event.pathParameters.funder); return response.json(200, funder); } catch (err) { diff --git a/api/src/components/link/controller.ts b/api/src/components/link/controller.ts index 330d5aab0..6c61cfb79 100644 --- a/api/src/components/link/controller.ts +++ b/api/src/components/link/controller.ts @@ -7,7 +7,7 @@ import * as I from 'interface'; export const create = async (event: I.AuthenticatedAPIRequest): Promise => { try { // function checks if the user has permission to see it in DRAFT mode - const fromPublication = await publicationService.getWithVersion(event.body.from); + const fromPublication = await publicationService.get(event.body.from); // the publication does not exist, is // publications that are live cannot have links created. @@ -17,16 +17,22 @@ export const create = async (event: I.AuthenticatedAPIRequest) }); } - const fromCurrentVersion = fromPublication.versions[0]; + const fromLatestVersion = fromPublication.versions.find((version) => version.isLatestVersion); - if (fromCurrentVersion.currentStatus === 'LIVE') { + if (!fromLatestVersion) { + return response.json(403, { + message: `Cannot find latest version of ${event.body.from}.` + }); + } + + if (fromLatestVersion.currentStatus === 'LIVE') { return response.json(403, { message: `Publication with id ${event.body.from} is LIVE.` }); } // the authenticated user is not the owner of the publication - if (fromCurrentVersion.user.id !== event.user.id) { + if (fromLatestVersion.user.id !== event.user.id) { return response.json(401, { message: 'You do not have permission to create publication links' }); } diff --git a/api/src/components/publication/__tests__/createPublication.test.ts b/api/src/components/publication/__tests__/createPublication.test.ts index 738a9e766..2c7203c62 100644 --- a/api/src/components/publication/__tests__/createPublication.test.ts +++ b/api/src/components/publication/__tests__/createPublication.test.ts @@ -1,7 +1,7 @@ import * as testUtils from 'lib/testUtils'; describe('Create publication', () => { - beforeEach(async () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); @@ -22,12 +22,13 @@ describe('Create publication', () => { expect(createPublicationRequest.status).toEqual(201); - expect(createPublicationRequest.body.user.id).toEqual('test-user-1'); - expect(createPublicationRequest.body.publicationStatus.length).toEqual(1); - expect(createPublicationRequest.body.publicationStatus[0].status).toEqual('DRAFT'); - expect(createPublicationRequest.body.keywords.length).toEqual(2); - expect(createPublicationRequest.body.description).toEqual('description of Publication test 1'); - expect(createPublicationRequest.body.licence).toEqual('CC_BY_SA'); + expect(createPublicationRequest.body.versions.length).toEqual(1); + + expect(createPublicationRequest.body.versions[0].createdBy).toEqual('test-user-1'); + expect(createPublicationRequest.body.versions[0].currentStatus).toEqual('DRAFT'); + expect(createPublicationRequest.body.versions[0].keywords.length).toEqual(2); + expect(createPublicationRequest.body.versions[0].description).toEqual('description of Publication test 1'); + expect(createPublicationRequest.body.versions[0].licence).toEqual('CC_BY_SA'); }); test('Valid publication created by real user with content (200)', async () => { @@ -127,7 +128,7 @@ describe('Create publication', () => { }); expect(createPublicationRequest.status).toEqual(201); - expect(createPublicationRequest.body.publishedDate).toBeNull(); + expect(createPublicationRequest.body.versions[0].publishedDate).toBeNull(); }); test('Valid publicatiom created by real user when provided a correct ISO-639-1 language code', async () => { @@ -144,7 +145,7 @@ describe('Create publication', () => { }); expect(createPublicationRequest.status).toEqual(201); - expect(createPublicationRequest.body.language).toEqual('fr'); + expect(createPublicationRequest.body.versions[0].language).toEqual('fr'); }); test('Publication failed to be created if language code provided is not out of the ISO-639-1 language list', async () => { @@ -208,7 +209,7 @@ describe('Create publication', () => { }); expect(createPublicationRequest.status).toEqual(201); - expect(createPublicationRequest.body.language).toEqual('en'); + expect(createPublicationRequest.body.versions[0].language).toEqual('en'); }); test('Publication can not be created if supplying a self declaration and if not a protocol or hypotheses', async () => { @@ -240,7 +241,7 @@ describe('Create publication', () => { expect(createPublicationRequest.status).toEqual(201); expect(createPublicationRequest.body.type).toEqual('PROTOCOL'); - expect(createPublicationRequest.body.selfDeclaration).toEqual(true); + expect(createPublicationRequest.body.versions[0].selfDeclaration).toEqual(true); }); test('Publication can be created if not supplying a self declration and is of type hypotheses', async () => { @@ -257,7 +258,7 @@ describe('Create publication', () => { expect(createPublicationRequest.status).toEqual(201); expect(createPublicationRequest.body.type).toEqual('HYPOTHESIS'); - expect(createPublicationRequest.body.selfDeclaration).toEqual(true); + expect(createPublicationRequest.body.versions[0].selfDeclaration).toEqual(true); }); test('Publication can be linked to topic on creation', async () => { diff --git a/api/src/components/publication/__tests__/deletePublication.test.ts b/api/src/components/publication/__tests__/deletePublication.test.ts deleted file mode 100644 index f5a30dd0f..000000000 --- a/api/src/components/publication/__tests__/deletePublication.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as testUtils from 'lib/testUtils'; -import * as client from 'lib/client'; - -describe('Delete publications', () => { - beforeEach(async () => { - await testUtils.clearDB(); - await testUtils.testSeed(); - }); - - test('User can delete their own DRAFT publication', async () => { - const getPublication = await testUtils.agent.delete('/publications/publication-problem-draft').query({ - apiKey: '000000005' - }); - - expect(getPublication.status).toEqual(200); - - const checkForPublication = await client.prisma.publication.count({ - where: { - id: 'publication-problem-draft' - } - }); - - expect(checkForPublication).toEqual(0); - }); - - test('User cannot delete their own LIVE publication', async () => { - const getPublication = await testUtils.agent.delete('/publications/publication-problem-live').query({ - apiKey: '123456789' - }); - - expect(getPublication.status).toEqual(403); - - const checkForPublication = await client.prisma.publication.count({ - where: { - id: 'publication-problem-live' - } - }); - - expect(checkForPublication).toEqual(1); - }); - - test('User cannot delete a DRAFT publication they did not create', async () => { - const getPublication = await testUtils.agent.delete('/publications/publication-problem-draft').query({ - apiKey: '987654321' - }); - - expect(getPublication.status).toEqual(403); - }); - - test('User cannot delete a LIVE publication they did not create', async () => { - const getPublication = await testUtils.agent.delete('/publications/publication-problem-live').query({ - apiKey: '987654321' - }); - - expect(getPublication.status).toEqual(403); - }); - - test('Unauthenticated user cannot delete a DRAFT publication they did not create', async () => { - const getPublication = await testUtils.agent.delete('/publications/publication-problem-draft'); - - expect(getPublication.status).toEqual(401); - }); -}); diff --git a/api/src/components/publication/__tests__/getPublication.test.ts b/api/src/components/publication/__tests__/getPublication.test.ts index 9dc7030b1..5539ffd9e 100644 --- a/api/src/components/publication/__tests__/getPublication.test.ts +++ b/api/src/components/publication/__tests__/getPublication.test.ts @@ -1,33 +1,34 @@ import * as testUtils from 'lib/testUtils'; -describe('View individual publications', () => { +describe('View publications + versions', () => { beforeEach(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); - test('User who created publication can see DRAFT publication', async () => { + test('User who created publication version can see DRAFT versions', async () => { const getPublication = await testUtils.agent.get('/publications/publication-1').query({ apiKey: '123456789' }); expect(getPublication.body.id).toEqual('publication-1'); + expect(getPublication.body.versions.some((version) => version.currentStatus === 'DRAFT')); }); - test('User who did not create publication cannot see DRAFT publication', async () => { + test('User who did not create publication version cannot see DRAFT versions', async () => { const getPublication = await testUtils.agent.get('/publications/publication-1').query({ apiKey: '987654321' }); - expect(getPublication.status).toEqual(404); + expect(getPublication.status).toEqual(403); }); - test('Cannot view publication in DRAFT without API key', async () => { + test('Cannot view publication version in DRAFT without API key', async () => { const getPublication = await testUtils.agent.get('/publications/publication-1').query({ apiKey: '987654321' }); - expect(getPublication.status).toEqual(404); + expect(getPublication.status).toEqual(403); }); test.todo('Any user can see a LIVE publication'); diff --git a/api/src/components/publication/__tests__/updateStatus.test.ts b/api/src/components/publication/__tests__/updateStatus.test.ts deleted file mode 100644 index 2122fd0cb..000000000 --- a/api/src/components/publication/__tests__/updateStatus.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as testUtils from 'lib/testUtils'; - -beforeEach(async () => { - await testUtils.clearDB(); - await testUtils.testSeed(); -}); - -describe('Update publication status', () => { - test('User with permissions can update their publication to LIVE from DRAFT (after creating a link)', async () => { - const updatePublicationAttemptOne = await testUtils.agent - .put('/publications/publication-analysis-draft/status/LIVE') - .query({ - apiKey: '123456789' - }); - - expect(updatePublicationAttemptOne.status).toEqual(403); - - // add a valid link - await testUtils.agent - .post('/links') - .query({ - apiKey: '123456789' - }) - .send({ - from: 'publication-analysis-draft', - to: 'publication-data-live' - }); - - const updatePublicationAttemptTwo = await testUtils.agent - .put('/publications/publication-analysis-draft/status/LIVE') - .query({ - apiKey: '123456789' - }); - - expect(updatePublicationAttemptTwo.status).toEqual(200); - }); - - test('User with permissions can update their publication to LIVE from DRAFT', async () => { - const updatedPublication = await testUtils.agent - .put('/publications/publication-hypothesis-draft-problem-live/status/LIVE') - .query({ - apiKey: '123456789' - }); - - expect(updatedPublication.status).toEqual(200); - }); - - test('User with permissions cannot update their publication to DRAFT from LIVE', async () => { - const updatedPublication = await testUtils.agent - .put('/publications/publication-problem-live/status/DRAFT') - .query({ - apiKey: '123456789' - }); - - expect(updatedPublication.status).toEqual(403); - }); - - test('User without permissions cannot update their publication to LIVE from DRAFT', async () => { - const updatedPublication = await testUtils.agent - .put('/publications/publication-hypothesis-draft-problem-live/status/LIVE') - .query({ - apiKey: '987654321' - }); - - expect(updatedPublication.status).toEqual(403); - }); - - test('User with permissions cannot update their publication to LIVE from DRAFT if there is no content.', async () => { - const updatedPublication = await testUtils.agent - .put('/publications/publication-problem-draft-no-content/status/LIVE') - .query({ - apiKey: '123456789' - }); - - expect(updatedPublication.status).toEqual(403); - }); - - test('User with permissions cannot update their publication to LIVE from DRAFT if there is no licence.', async () => { - const updatedPublication = await testUtils.agent - .put('/publications/publication-hypothesis-draft/status/LIVE') - .query({ - apiKey: '000000005' - }); - - expect(updatedPublication.status).toEqual(403); - }); - - test('User with permissions can update their publication to LIVE from DRAFT and a publishedDate is created', async () => { - const updatedPublication = await testUtils.agent - .put('/publications/publication-hypothesis-draft-problem-live/status/LIVE') - .query({ - apiKey: '123456789' - }); - - expect(updatedPublication.status).toEqual(200); - expect(updatedPublication.body.message).toEqual('Publication is now LIVE.'); - }); - - // COI tests - test('User with permissions cannot update their publication to LIVE if they have a conflict of interest, but have not provided coi text', async () => { - const updatedPublication = await testUtils.agent - .put('/publications/publication-problem-draft-with-coi-but-no-text/status/LIVE') - .query({ - apiKey: '123456789' - }); - - expect(updatedPublication.status).toEqual(403); - }); - - test('User with permissions can update their publication to LIVE with a conflict of interest, if they have provided text', async () => { - const updatedPublication = await testUtils.agent - .put('/publications/publication-problem-draft-with-coi-with-text/status/LIVE') - .query({ - apiKey: '123456789' - }); - - expect(updatedPublication.status).toEqual(200); - }); - - test('User with permissions can update their publication to LIVE if they have no conflict of interest & have not provided text', async () => { - const updatedPublication = await testUtils.agent - .put('/publications/publication-problem-draft-with-no-coi-with-no-text/status/LIVE') - .query({ - apiKey: '123456789' - }); - - expect(updatedPublication.status).toEqual(200); - }); - - test('User with permissions can update their publication to LIVE if they have no conflict of interest & have provided text', async () => { - const updatedPublication = await testUtils.agent - .put('/publications/publication-problem-draft-with-no-coi-with-text/status/LIVE') - .query({ - apiKey: '123456789' - }); - - expect(updatedPublication.status).toEqual(200); - }); - - test('Publication owner can publish if all co-authors are confirmed', async () => { - const updatePublication = await testUtils.agent - .put('/publications/publication-protocol-draft/status/LIVE') - .query({ - apiKey: '000000005' - }); - - expect(updatePublication.status).toEqual(200); - - expect(updatePublication.body.message).toEqual('Publication is now LIVE.'); - }); - - test('Publication owner cannot publish if not all co-authors are confirmed', async () => { - const updatePublication = await testUtils.agent - .put('/publications/publication-hypothesis-draft/status/LIVE') - .query({ - apiKey: '000000005' - }); - - expect(updatePublication.status).toEqual(403); - expect(updatePublication.body.message).toEqual( - 'Publication is not ready to be made LIVE. Make sure all fields are filled in.' - ); - - const getPublicationStatus = await testUtils.agent.get('/publications/publication-hypothesis-draft').query({ - apiKey: '000000005' - }); - - expect(getPublicationStatus.body.currentStatus).toEqual('DRAFT'); - }); - - test('User other than the owner (does not have permission) cannot publish if co-authors all approved', async () => { - const updatePublication = await testUtils.agent - .put('/publications/publication-hypothesis-draft/status/LIVE') - .query({ - apiKey: '000000006' - }); - - expect(updatePublication.status).toEqual(403); - expect(updatePublication.body.message).toEqual( - 'You do not have permission to modify the status of this publication.' - ); - - const getPublicationStatus = await testUtils.agent.get('/publications/publication-hypothesis-draft').query({ - apiKey: '000000005' - }); - - expect(getPublicationStatus.body.currentStatus).toEqual('DRAFT'); - }); - - test('Publication owner cannot update publication status to LOCKED if there are no co-authors', async () => { - const response = await testUtils.agent.put('/publications/publication-2/status/LOCKED').query({ - apiKey: '987654321' - }); - - expect(response.status).toEqual(403); - expect(response.body.message).toEqual( - 'Publication is not ready to be LOCKED. Make sure all fields are filled in.' - ); - }); - - test('Throws an error if trying to update publication status to the same status', async () => { - const response = await testUtils.agent.put('/publications/publication-2/status/DRAFT').query({ - apiKey: '987654321' - }); - - expect(response.status).toEqual(403); - expect(response.body.message).toEqual('Publication status is already DRAFT.'); - }); - - test('Publication status can be updated from DRAFT to LOCKED only after requesting approvals', async () => { - // try to update status to LOCKED - const updateStatusResponse1 = await testUtils.agent - .put('/publications/publication-problem-draft/status/LOCKED') - .query({ - apiKey: '000000005' - }); - - expect(updateStatusResponse1.status).toEqual(403); - expect(updateStatusResponse1.body.message).toEqual( - 'Publication is not ready to be LOCKED. Make sure all fields are filled in.' - ); - - // request co-authors approvals - const requestApprovalsResponse = await testUtils.agent - .put('/publicationVersions/publication-problem-draft-v1/coauthors/request-approval') - .query({ - apiKey: '000000005' - }); - - expect(requestApprovalsResponse.status).toEqual(200); - - // try to update status to LOCKED again - const updateStatusResponse2 = await testUtils.agent - .put('/publications/publication-problem-draft/status/LOCKED') - .query({ - apiKey: '000000005' - }); - - expect(updateStatusResponse2.status).toEqual(200); - expect(updateStatusResponse2.body.message).toEqual('Publication status updated to LOCKED.'); - }); - - test('Publication status can be updated from LOCKED to LIVE after all co-authors approved', async () => { - const response = await testUtils.agent - .put('/publications/locked-publication-problem-confirmed-co-authors/status/LIVE') - .query({ - apiKey: '123456789' - }); - - expect(response.status).toEqual(200); - expect(response.body.message).toEqual('Publication is now LIVE.'); - }); -}); diff --git a/api/src/components/publication/controller.ts b/api/src/components/publication/controller.ts index 9493d3380..45f87003d 100644 --- a/api/src/components/publication/controller.ts +++ b/api/src/components/publication/controller.ts @@ -1,74 +1,36 @@ -import htmlToText from 'html-to-text'; import axios from 'axios'; import * as s3 from 'lib/s3'; -import * as sqs from 'lib/sqs'; import * as I from 'interface'; import * as helpers from 'lib/helpers'; import * as response from 'lib/response'; import * as publicationService from 'publication/service'; import * as publicationVersionService from 'publicationVersion/service'; -import * as referenceService from 'reference/service'; -import * as coAuthorService from 'coauthor/service'; - -export const getAll = async ( - event: I.AuthenticatedAPIRequest -): Promise => { - try { - const openSearchPublications = await publicationService.getOpenSearchRecords(event.queryStringParameters); - - const publicationIds = openSearchPublications.body.hits.hits.map((hit) => hit._id as string); - - const publications = await publicationService.getAllByIds(publicationIds); - - const publicationsOrderedBySearch = publicationIds.map((publicationId) => - publications.find((publication) => publication.id === publicationId) - ); - - return response.json(200, { - data: publicationsOrderedBySearch, - metadata: { - total: openSearchPublications.body.hits.total.value, - limit: Number(event.queryStringParameters.limit) || 10, - offset: Number(event.queryStringParameters.offset) || 0 - } - }); - } catch (err) { - console.log(err); - - return response.json(500, { message: 'Unknown server error.' }); - } -}; export const get = async ( event: I.APIRequest ): Promise => { try { - // Get the publication with the latest version data merged in to keep it simple for the UI. - const publication = await publicationService.getWithVersionMerged(event.pathParameters.id); - - // anyone can see a LIVE publication - if (publication?.currentStatus === 'LIVE') { - return response.json(200, publication); - } + const publication = await publicationService.get(event.pathParameters.id); if (!publication) { return response.json(404, { - message: - 'Publication is either not found, or you do not have permissions to view it in its current state.' + message: 'Publication not found.' }); } - // only the owner or co-authors can view publications - if ( - event.user?.id === publication.user.id || - publication.coAuthors.some((coAuthor) => coAuthor.linkedUser === event.user?.id) - ) { - return response.json(200, publication); + // only the owner or co-authors can view the DRAFT/LOCKED versions + publication.versions = publication.versions.filter((version) => + version.currentStatus === 'LIVE' + ? true + : event.user?.id === version.createdBy || + version.coAuthors.some((author) => author.linkedUser === event.user?.id) + ); + + if (!publication.versions.length) { + return response.json(403, { message: "You don't have permissions to view this publication." }); } - return response.json(404, { - message: 'Publication is either not found, or you do not have permissions to view it in its current state.' - }); + return response.json(200, publication); } catch (err) { console.log(err); @@ -92,57 +54,6 @@ export const getSeedDataPublications = async ( } }; -export const deletePublication = async ( - event: I.AuthenticatedAPIRequest -): Promise => { - try { - const publication = await publicationService.get(event.pathParameters.id); - - if (!publication) { - return response.json(403, { - message: 'This publication does not exist.' - }); - } - - // If there has been more than one version of a publication, we can't delete it. - if (publication.versions.length > 1) { - return response.json(403, { - message: 'A publication can not be deleted if there is more than one version of it.' - }); - } else if (!publication.versions || publication.versions.length === 0) { - throw Error('Could not get versions for publication'); - } - - const version = publication.versions[0]; - - if (version.user.id !== event.user.id) { - return response.json(403, { - message: 'You do not have permission to delete this publication.' - }); - } - - // The logic here is a bit odd, but the currentStatus and publicationStatus array are not intrinsically linked - // so to be safe, we are checking that the current status is DRAFT and that the entire history of the publication - // has only ever been draft. - if ( - version.currentStatus !== 'DRAFT' || - (version.publicationStatus && !version.publicationStatus.every((status) => status.status !== 'LIVE')) - ) { - return response.json(403, { - message: 'A publication can only be deleted if it is currently a draft and has never been LIVE.' - }); - } - - await publicationService.deletePublication(event.pathParameters.id); - - return response.json(200, { message: `Publication ${event.pathParameters.id} deleted` }); - } catch (err) { - console.log(err); - - return response.json(500, { message: 'Unknown server error.' }); - } -}; - export const create = async ( event: I.AuthenticatedAPIRequest ): Promise => { @@ -181,201 +92,33 @@ export const create = async ( } }; -export const updateCurrentVersion = async ( - event: I.AuthenticatedAPIRequest +export const getLinksForPublication = async ( + event: I.APIRequest ): Promise => { - try { - const publication = await publicationService.getWithVersion(event.pathParameters.id); - - if (!publication) { - return response.json(403, { - message: 'This publication does not exist.' - }); - } - - const currentVersion = publication.versions[0]; - - if (!currentVersion) { - throw Error('Unable to find current version for publication'); - } - - if (currentVersion.user.id !== event.user.id) { - return response.json(403, { - message: 'You do not have permission to modify this publication.' - }); - } - - if (currentVersion.currentStatus !== 'DRAFT') { - return response.json(404, { message: 'A publication that is not in DRAFT state cannot be updated.' }); - } - - if (event.body.content) { - event.body.content = helpers.getSafeHTML(event.body.content); - } - - if ( - event.body.selfDeclaration !== undefined && - publication.type !== 'PROTOCOL' && - publication.type !== 'HYPOTHESIS' - ) { - return response.json(400, { - message: - 'You can not declare a self declaration for a publication that is not a protocol or hypothesis.' - }); - } - - if (event.body.dataAccessStatement !== undefined && publication.type !== 'DATA') { - return response.json(400, { - message: 'You can not supply a data access statement on a non data publication.' - }); - } - - if (event.body.dataPermissionsStatement !== undefined && publication.type !== 'DATA') { - return response.json(400, { - message: 'You can not supply a data permissions statement on a non data publication.' - }); - } - - await publicationService.updateCurrentVersion(event.pathParameters.id, event.body); - - const updatedPublication = await publicationService.getWithVersionMerged(event.pathParameters.id); - - return response.json(200, updatedPublication); - } catch (err) { - console.log(err); - - return response.json(500, { message: 'Unknown server error.' }); - } -}; + const publicationId = event.pathParameters.id; + const directLinks = event.queryStringParameters?.direct === 'true'; + const user = event.user; + let includeDraft = false; -export const updateStatus = async ( - event: I.AuthenticatedAPIRequest -): Promise => { try { - const publicationId = event.pathParameters?.id; - const publication = await publicationService.getWithVersion(publicationId); - - if (!publication) { - return response.json(404, { - message: 'This publication does not exist.' - }); - } - - const currentVersion = publication.versions[0]; - - if (currentVersion.createdBy !== event.user.id) { - return response.json(403, { - message: 'You do not have permission to modify the status of this publication.' - }); - } - - const newStatus = event.pathParameters?.status; - const currentStatus = currentVersion.currentStatus; - - if (currentStatus === 'LIVE') { - return response.json(403, { - message: 'A status of a publication that is not in DRAFT or LOCKED cannot be changed.' - }); - } - - if (currentStatus === newStatus) { - return response.json(403, { message: `Publication status is already ${newStatus}.` }); - } - - if (currentStatus === 'DRAFT') { - if (newStatus === 'LOCKED') { - // check if publication version actually has co-authors - if (currentVersion.coAuthors.length === 1) { - return response.json(403, { message: 'Publication cannot be LOCKED without co-authors.' }); - } - - // check if publication version is ready to be LOCKED - if (!publicationService.isReadyToLock(publication)) { - return response.json(403, { - message: 'Publication is not ready to be LOCKED. Make sure all fields are filled in.' - }); - } - - // Lock publication from editing - await publicationVersionService.updateStatus(currentVersion.id, 'LOCKED'); - - return response.json(200, { message: 'Publication status updated to LOCKED.' }); - } - - if (newStatus === 'LIVE') { - const isReadyToPublish = publicationService.isReadyToPublish(publication); - - if (!isReadyToPublish) { - return response.json(403, { - message: 'Publication is not ready to be made LIVE. Make sure all fields are filled in.' - }); + if (directLinks) { + if (user) { + const latestVersion = await publicationVersionService.get(publicationId, 'latest'); + + // if latest version is a DRAFT, check if user can see it + if ( + latestVersion?.currentStatus === 'DRAFT' && + (user.id === latestVersion?.createdBy || + latestVersion?.coAuthors.some((coAuthor) => coAuthor.linkedUser === user.id)) + ) { + includeDraft = true; } } } - if (currentStatus === 'LOCKED') { - if (newStatus === 'DRAFT') { - // Update status to 'DRAFT' - await publicationVersionService.updateStatus(currentVersion.id, newStatus); - - // Cancel co author approvals - await coAuthorService.resetCoAuthors(currentVersion.id); - - return response.json(200, { - message: 'Publication unlocked for editing' - }); - } - - if (newStatus === 'LIVE') { - const isReadyToPublish = publicationService.isReadyToPublish(publication); - - if (!isReadyToPublish) { - return response.json(403, { - message: 'Publication is not ready to be made LIVE. Make sure all fields are filled in.' - }); - } - } - } - - const updatedVersion = await publicationVersionService.updateStatus(currentVersion.id, newStatus); - - // now that the publication version is LIVE, add/update the opensearch record - await publicationService.createOpenSearchRecord({ - id: publicationId, - type: updatedVersion.publication.type, - title: updatedVersion.title, - licence: updatedVersion.licence, - description: updatedVersion.description, - keywords: updatedVersion.keywords, - content: updatedVersion.content, - publishedDate: updatedVersion.publishedDate, - cleanContent: htmlToText.convert(updatedVersion.content) - }); - - const references = await referenceService.getAllByPublicationVersion(updatedVersion.id); - - // Publication version is live, so update the DOI - await helpers.updateDOI(publication.doi, publication, references); - - // send message to the pdf generation queue - // currently only on deployed instances while a local solution is developed - if (process.env.STAGE !== 'local') await sqs.sendMessage(publicationId); - - return response.json(200, { message: 'Publication is now LIVE.' }); - } catch (err) { - console.log(err); - - return response.json(500, { message: 'Unknown server error.' }); - } -}; - -export const getLinksForPublication = async ( - event: I.APIRequest -): Promise => { - try { - const { publication, linkedFrom, linkedTo } = await publicationService.getLinksForPublication( - event.pathParameters.id - ); + const { publication, linkedFrom, linkedTo } = directLinks + ? await publicationService.getDirectLinksForPublication(publicationId, includeDraft) + : await publicationService.getLinksForPublication(publicationId); if (!publication) { return response.json(404, { message: 'Not found.' }); @@ -429,22 +172,13 @@ export const getPDF = async ( // generate new PDF try { // We know the publication has at least one LIVE version. - const latestPublishedVersion = publication.versions.find((version) => version.isLatestLiveVersion); + const latestPublishedVersion = await publicationVersionService.get(publication.id, 'latestLive'); if (!latestPublishedVersion) { throw Error('Unable to get latest published version from supplied object'); } - const publicationWithLatestPublishedVersion = await publicationService.getWithVersion( - publication.id, - latestPublishedVersion.versionNumber - ); - - if (!publicationWithLatestPublishedVersion) { - throw Error('Unable to get latest published version from DB'); - } - - const newPDFUrl = await publicationService.generatePDF(publicationWithLatestPublishedVersion); + const newPDFUrl = await publicationService.generatePDF(latestPublishedVersion); if (!newPDFUrl) { throw Error('Failed to generate PDF'); @@ -527,3 +261,17 @@ export const updateTopics = async ( return response.json(500, { message: 'Unknown server error.' }); } }; + +export const getPublicationTopics = async ( + event: I.APIRequest +): Promise => { + try { + const topics = await publicationService.getPublicationTopics(event.pathParameters.id); + + return response.json(200, topics); + } catch (error) { + console.log(error); + + return response.json(500, { message: 'Unknown server error.' }); + } +}; diff --git a/api/src/components/publication/routes.ts b/api/src/components/publication/routes.ts index 859256e98..20218c154 100644 --- a/api/src/components/publication/routes.ts +++ b/api/src/components/publication/routes.ts @@ -1,15 +1,9 @@ import middy from '@middy/core'; import * as middleware from 'middleware'; - import * as publicationController from 'publication/controller'; import * as publicationSchema from 'publication/schema'; -export const getAll = middy(publicationController.getAll) - .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) - .use(middleware.httpJsonBodyParser()) - .use(middleware.validator(publicationSchema.getAll, 'queryStringParameters')); - export const get = middy(publicationController.get) .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) .use(middleware.httpJsonBodyParser()) @@ -25,23 +19,6 @@ export const create = middy(publicationController.create) .use(middleware.authentication()) .use(middleware.validator(publicationSchema.create, 'body')); -export const update = middy(publicationController.updateCurrentVersion) - .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) - .use(middleware.httpJsonBodyParser()) - .use(middleware.authentication()) - .use(middleware.validator(publicationSchema.update, 'body')); - -export const deletePublication = middy(publicationController.deletePublication) - .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) - .use(middleware.httpJsonBodyParser()) - .use(middleware.authentication()); - -export const updateStatus = middy(publicationController.updateStatus) - .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) - .use(middleware.httpJsonBodyParser()) - .use(middleware.authentication()) - .use(middleware.validator(publicationSchema.updateStatus, 'pathParameters')); - export const getPublicationLinks = middy(publicationController.getLinksForPublication) .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) .use(middleware.httpJsonBodyParser()) @@ -61,3 +38,7 @@ export const updateTopics = middy(publicationController.updateTopics) .use(middleware.httpJsonBodyParser()) .use(middleware.authentication()) .use(middleware.validator(publicationSchema.updateTopics, 'body')); + +export const getPublicationTopics = middy(publicationController.getPublicationTopics).use( + middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true }) +); diff --git a/api/src/components/publication/schema/index.ts b/api/src/components/publication/schema/index.ts index f88330e35..77bca4bb6 100644 --- a/api/src/components/publication/schema/index.ts +++ b/api/src/components/publication/schema/index.ts @@ -1,6 +1,3 @@ export { default as create } from './create'; -export { default as updateStatus } from './updateStatus'; -export { default as getAll } from './getAll'; -export { default as update } from './update'; export { default as getPDF } from './getPDF'; export { default as updateTopics } from './updateTopics'; diff --git a/api/src/components/publication/service.ts b/api/src/components/publication/service.ts index 5ad5a4685..0690d05db 100644 --- a/api/src/components/publication/service.ts +++ b/api/src/components/publication/service.ts @@ -6,97 +6,8 @@ import * as referenceService from 'reference/service'; import * as Helpers from 'lib/helpers'; import { Prisma } from '@prisma/client'; import { Browser, launch } from 'puppeteer-core'; - import { PutObjectCommand } from '@aws-sdk/client-s3'; -import * as publicationVersionService from 'publicationVersion/service'; - -export const getAllByIds = async (ids: Array) => { - // Get base publications - const publications = await client.prisma.publication.findMany({ - where: { - id: { - in: ids - } - } - }); - - // Get current versions of these publications - const versions = await client.prisma.publicationVersion.findMany({ - where: { - versionOf: { - in: ids - }, - isLatestVersion: true - }, - include: { - user: { - select: { - firstName: true, - lastName: true, - id: true, - orcid: true - } - }, - coAuthors: { - select: { - id: true, - approvalRequested: true, - confirmedCoAuthor: true, - code: true, - linkedUser: true, - email: true, - publicationVersionId: true, - user: { - select: { - orcid: true, - firstName: true, - lastName: true - } - } - }, - orderBy: { - position: 'asc' - } - } - } - }); - - if (publications.length !== versions.length) { - throw Error('Unable to find a current version for all requested publications'); - } - - // Merge versioned data into the publication records - const mergedPublications = publications.map((publication) => { - const currentVersion = versions.find((version) => version.versionOf === publication.id); - - return { ...currentVersion, ...publication }; - }); - - return mergedPublications; -}; - -export const updateCurrentVersion = async (id: string, updateContent: I.UpdatePublicationRequestBody) => { - // Updates will always be made to the current version. - const currentVersion = await client.prisma.publicationVersion.findFirst({ - where: { - versionOf: id, - isLatestVersion: true - }, - select: { - id: true - } - }); - const updatedVersion = await client.prisma.publicationVersion.update({ - where: { - id: currentVersion?.id - }, - data: updateContent - }); - - return updatedVersion; -}; - export const isIdInUse = async (id: string) => { const publication = await client.prisma.publication.count({ where: { @@ -107,375 +18,6 @@ export const isIdInUse = async (id: string) => { return Boolean(publication); }; -// For convenience, sometimes we want to present a publication with the data from -// a particular version inline, disguised as one entity. For example, when we provide it straight to the UI. -export const getWithVersionMerged = async (id: string, versionNumber?: number) => { - // Get the overall publication without versions initially - const publication = await client.prisma.publication.findFirst({ - where: { - id - }, - include: { - publicationFlags: { - select: { - id: true, - category: true, - resolved: true, - createdBy: true, - createdAt: true, - user: { - select: { - id: true, - orcid: true, - firstName: true, - lastName: true, - email: true, - createdAt: true, - updatedAt: true - } - } - } - }, - linkedTo: { - where: { - publicationToRef: { - versions: { - some: { - isLatestLiveVersion: true - } - } - } - }, - select: { - id: true, - publicationToRef: { - select: { - id: true, - type: true, - doi: true, - versions: { - select: { - title: true, - publishedDate: true, - currentStatus: true, - description: true, - keywords: true, - user: { - select: { - id: true, - firstName: true, - lastName: true, - orcid: true - } - } - } - } - } - } - } - }, - linkedFrom: { - where: { - publicationFromRef: { - versions: { - some: { - isLatestLiveVersion: true - } - } - } - }, - select: { - id: true, - publicationFromRef: { - select: { - id: true, - type: true, - doi: true, - versions: { - select: { - title: true, - publishedDate: true, - currentStatus: true, - description: true, - keywords: true, - user: { - select: { - id: true, - firstName: true, - lastName: true, - orcid: true - } - } - } - } - } - } - } - }, - topics: { - select: { - id: true, - title: true, - language: true, - translations: true - } - } - } - }); - - // Get the specified version if we are given a version number, otherwise the current one. - const versionWhere = { - versionOf: id, - ...(versionNumber - ? { - versionNumber - } - : { - isLatestVersion: true - }) - }; - - const version = await client.prisma.publicationVersion.findFirst({ - where: versionWhere, - include: { - user: { - select: { - id: true, - orcid: true, - firstName: true, - lastName: true, - email: true, - createdAt: true, - updatedAt: true - } - }, - publicationStatus: { - select: { - status: true, - createdAt: true, - id: true - }, - orderBy: { - createdAt: 'desc' - } - }, - funders: { - select: { - id: true, - city: true, - country: true, - name: true, - link: true, - ror: true - } - }, - coAuthors: { - select: { - id: true, - email: true, - linkedUser: true, - publicationVersionId: true, - confirmedCoAuthor: true, - approvalRequested: true, - createdAt: true, - reminderDate: true, - isIndependent: true, - affiliations: true, - user: { - select: { - firstName: true, - lastName: true, - orcid: true - } - } - }, - orderBy: { - position: 'asc' - } - } - } - }); - - if (!version || !publication) { - return null; - } - - // Discard versionOf field - const { versionOf, ...versionRest } = version; - - // Necessary to name version id as versionId because "id" will be overwritten - // by spread operator with publication's id - return { ...versionRest, versionId: version?.id, ...publication }; -}; - -// Get a publication with a version attached. By default, the current version. -export const getWithVersion = async (id: string, versionNumber?: number) => { - return await client.prisma.publication.findFirst({ - where: { - id - }, - include: { - versions: { - where: { - versionOf: id, - ...(versionNumber - ? { - versionNumber - } - : { - isLatestVersion: true - }) - }, - include: { - user: { - select: { - id: true, - orcid: true, - firstName: true, - lastName: true, - email: true, - createdAt: true, - updatedAt: true - } - }, - publicationStatus: { - select: { - status: true, - createdAt: true, - id: true - }, - orderBy: { - createdAt: 'desc' - } - }, - funders: { - select: { - id: true, - city: true, - country: true, - name: true, - link: true, - ror: true - } - }, - coAuthors: { - select: { - id: true, - email: true, - linkedUser: true, - publicationVersionId: true, - confirmedCoAuthor: true, - approvalRequested: true, - createdAt: true, - reminderDate: true, - isIndependent: true, - affiliations: true, - user: { - select: { - firstName: true, - lastName: true, - orcid: true - } - } - }, - orderBy: { - position: 'asc' - } - } - } - }, - publicationFlags: { - select: { - id: true, - category: true, - resolved: true, - createdBy: true, - createdAt: true, - user: { - select: { - id: true, - orcid: true, - firstName: true, - lastName: true, - email: true, - createdAt: true, - updatedAt: true - } - } - } - }, - linkedTo: { - where: { - publicationToRef: { - versions: { - some: { - isLatestLiveVersion: true - } - } - } - }, - select: { - id: true, - publicationToRef: { - select: { - id: true, - type: true, - doi: true, - versions: { - select: { - title: true, - publishedDate: true, - currentStatus: true, - description: true, - keywords: true - } - } - } - } - } - }, - linkedFrom: { - where: { - publicationFromRef: { - versions: { - some: { - isLatestLiveVersion: true - } - } - } - }, - select: { - id: true, - publicationFromRef: { - select: { - id: true, - type: true, - doi: true, - versions: { - select: { - title: true, - publishedDate: true, - currentStatus: true, - description: true, - keywords: true - } - } - } - } - } - }, - topics: { - select: { - id: true, - title: true, - language: true, - translations: true - } - } - } - }); -}; - export const get = async (id: string) => { return await client.prisma.publication.findUnique({ where: { @@ -686,7 +228,7 @@ export const createOpenSearchRecord = async (data: I.OpenSearchPublication) => { return publication; }; -export const getOpenSearchRecords = async (filters: I.PublicationFilters) => { +export const getOpenSearchPublications = async (filters: I.OpenSearchPublicationFilters) => { const orderBy = filters.orderBy ? { [filters.orderBy]: { @@ -801,141 +343,30 @@ export const create = async (e: I.CreatePublicationRequestBody, user: I.User, do status: 'DRAFT' } }, - coAuthors: { - // add main author to authors list - create: { - linkedUser: user.id, - email: user.email || '', - confirmedCoAuthor: true, - approvalRequested: false - } - } - } - }, - topics: e.topicIds?.length - ? { - connect: e.topicIds.map((topicId) => ({ id: topicId })) - } - : undefined - }, - include: { - topics: { - select: { - id: true, - title: true, - language: true, - translations: true - } - }, - versions: { - include: { - user: { - select: { - id: true, - firstName: true, - lastName: true - } - }, - publicationStatus: { - select: { - status: true, - createdAt: true, - id: true - }, - orderBy: { - createdAt: 'desc' + coAuthors: { + // add main author to authors list + create: { + linkedUser: user.id, + email: user.email || '', + confirmedCoAuthor: true, + approvalRequested: false } } } - } - } - }); - - // Return first version data with new publication. - const version = await client.prisma.publicationVersion.findFirst({ - where: { - versionOf: doiResponse.data.attributes.suffix - }, - select: { - id: true, - versionNumber: true, - isLatestVersion: true, - currentStatus: true, - publishedDate: true, - title: true, - licence: true, - conflictOfInterestStatus: true, - conflictOfInterestText: true, - ethicalStatement: true, - ethicalStatementFreeText: true, - dataPermissionsStatement: true, - dataPermissionsStatementProvidedBy: true, - dataAccessStatement: true, - selfDeclaration: true, - description: true, - keywords: true, - content: true, - language: true, - fundersStatement: true, - user: { - select: { - id: true, - firstName: true, - lastName: true - } - }, - publicationStatus: { - select: { - status: true, - createdAt: true, - id: true - }, - orderBy: { - createdAt: 'desc' - } }, - funders: { - select: { - id: true, - city: true, - country: true, - name: true, - link: true, - ror: true - } - }, - coAuthors: { - select: { - id: true, - email: true, - linkedUser: true, - publicationVersionId: true, - confirmedCoAuthor: true, - approvalRequested: true, - createdAt: true, - reminderDate: true, - isIndependent: true, - affiliations: true, - user: { - select: { - firstName: true, - lastName: true, - orcid: true - } - } - }, - orderBy: { - position: 'asc' - } - } + topics: e.topicIds?.length + ? { + connect: e.topicIds.map((topicId) => ({ id: topicId })) + } + : undefined + }, + include: { + topics: true, + versions: true } }); - if (!version || !publication) { - throw Error('Failed to find publication and/or latest version data'); - } - - return { ...version, versionId: version.id, ...publication }; + return publication; }; export const doesDuplicateFlagExist = async (publication, category, user) => { @@ -951,81 +382,6 @@ export const doesDuplicateFlagExist = async (publication, category, user) => { return flag; }; -export const isReadyToPublish = (publication: I.PublicationWithVersionAttached): boolean => { - if (!publication) { - return false; - } - - const version = publication.versions[0]; - - const hasAtLeastOneLinkOrTopic = - publication.linkedTo.length !== 0 || (publication.type === 'PROBLEM' && publication.topics.length !== 0); - const hasFilledRequiredFields = - ['title', 'licence'].every((field) => version[field]) && !Helpers.isEmptyContent(version.content || ''); - const conflictOfInterest = publicationVersionService.validateConflictOfInterest(version); - const hasPublishDate = Boolean(version.publishedDate); - const isDataAndHasEthicalStatement = publication.type === 'DATA' ? version.ethicalStatement !== null : true; - const isDataAndHasPermissionsStatement = - publication.type === 'DATA' ? version.dataPermissionsStatement !== null : true; - - const coAuthorsAreVerified = !!version.coAuthors.every( - (coAuthor) => coAuthor.confirmedCoAuthor && (coAuthor.isIndependent || coAuthor.affiliations.length) - ); - - return ( - hasAtLeastOneLinkOrTopic && - hasFilledRequiredFields && - conflictOfInterest && - !hasPublishDate && - isDataAndHasEthicalStatement && - isDataAndHasPermissionsStatement && - coAuthorsAreVerified && - version.isLatestVersion - ); -}; - -export const isReadyToRequestApproval = (publication: I.PublicationWithVersionAttached): boolean => { - const version = publication?.versions[0]; - - if (!publication || !version?.isLatestVersion || version?.currentStatus !== 'DRAFT') { - return false; - } - - const hasAtLeastOneLinkOrTopic = - publication.linkedTo.length !== 0 || (publication.type === 'PROBLEM' && publication.topics.length !== 0); - const hasFilledRequiredFields = - ['title', 'licence'].every((field) => version[field]) && !Helpers.isEmptyContent(version.content || ''); - const conflictOfInterest = publicationVersionService.validateConflictOfInterest(version); - const isDataAndHasEthicalStatement = publication.type === 'DATA' ? version.ethicalStatement !== null : true; - const isDataAndHasPermissionsStatement = - publication.type === 'DATA' ? version.dataPermissionsStatement !== null : true; - const hasConfirmedAffiliations = !!version.coAuthors.some( - (author) => author.linkedUser === version.createdBy && (author.isIndependent || author.affiliations.length) - ); - - return ( - hasAtLeastOneLinkOrTopic && - hasFilledRequiredFields && - conflictOfInterest && - isDataAndHasEthicalStatement && - isDataAndHasPermissionsStatement && - hasConfirmedAffiliations && - version.isLatestVersion - ); -}; - -export const isReadyToLock = (publication: I.PublicationWithVersionAttached): boolean => { - const version = publication?.versions[0]; - - if (!publication || version?.currentStatus !== 'DRAFT') { - return false; - } - - const hasRequestedApprovals = !!version.coAuthors.some((author) => author.approvalRequested); - - return isReadyToRequestApproval(publication) && hasRequestedApprovals; -}; - export const getLinksForPublication = async (id: string): Promise => { const publication = await get(id); @@ -1064,10 +420,12 @@ export const getLinksForPublication = async (id: string): Promise` WITH RECURSIVE to_left AS ( - SELECT "Links"."publicationFrom" "childPublication", + SELECT "Links"."id" "linkId", + "Links"."publicationFrom" "childPublication", "Links"."publicationTo" "id", "pfrom".type "childPublicationType", "pto".type, + "pto"."doi", "pto_version".title, "pto_version"."createdBy", "pto_version"."publishedDate", @@ -1093,10 +451,12 @@ export const getLinksForPublication = async (id: string): Promise` WITH RECURSIVE to_right AS ( - SELECT "Links"."publicationFrom" "id", + SELECT "Links"."id" "linkId", + "Links"."publicationFrom" "id", "Links"."publicationTo" "parentPublication", "pfrom".type, + "pfrom"."doi", "pto".type "parentPublicationType", "pfrom_version"."title", "pfrom_version"."createdBy", @@ -1159,9 +521,11 @@ export const getLinksForPublication = async (id: string): Promise ({ + id: author.id, + linkedUser: author.linkedUser, + user: { + orcid: author.user?.orcid || '', + firstName: author.user?.firstName || '', + lastName: author.user?.lastName || '' + } + })) + }, + linkedTo, + linkedFrom + }; +}; + +export const getDirectLinksForPublication = async ( + id: string, + includeDraft = false +): Promise => { + const publicationFilter: Prisma.PublicationVersionWhereInput = includeDraft + ? { isLatestVersion: true } + : { isLatestLiveVersion: true }; + + const publication = await client.prisma.publication.findUnique({ + where: { + id + }, + include: { + versions: { + where: { + ...publicationFilter + }, + include: { + coAuthors: { + include: { + user: true + } + }, + user: true + } + }, + linkedTo: { + where: { + publicationToRef: { + versions: { + some: { + isLatestLiveVersion: true + } + } + } + }, + select: { + id: true, + publicationToRef: { + select: { + id: true, + doi: true, + type: true, + versions: { + include: { + user: true + } + } + } + } + } + }, + linkedFrom: { + where: { + publicationFromRef: { + versions: { + some: { + isLatestLiveVersion: true + } + } + } + }, + select: { + id: true, + publicationFromRef: { + select: { + id: true, + doi: true, + type: true, + versions: { + include: { + user: true + } + } + } + } + } + } + } + }); + + if (!publication) { + return { + publication: null, + linkedFrom: [], + linkedTo: [] + }; + } + + const latestLiveVersion = publication.versions[0]; + + if (!latestLiveVersion) { + return { + publication: null, + linkedFrom: [], + linkedTo: [] + }; + } + + const linkedTo: I.LinkedToPublication[] = publication.linkedTo.map((link) => { + const { id: linkId, publicationToRef } = link; + const { id, type, versions, doi } = publicationToRef; + const { createdBy, user, currentStatus, publishedDate, title } = versions[0]; + + return { + id, + linkId, + type, + doi, + childPublication: publication.id, + childPublicationType: publication.type, + title: title || '', + createdBy, + authorFirstName: user.firstName, + authorLastName: user.lastName || '', + currentStatus, + publishedDate: publishedDate?.toISOString() || '', + authors: [] + }; + }); + + const linkedFrom: I.LinkedFromPublication[] = publication.linkedFrom.map((link) => { + const { id: linkId, publicationFromRef } = link; + const { id, type, versions, doi } = publicationFromRef; + const { createdBy, user, currentStatus, publishedDate, title } = versions[0]; + + return { + id, + linkId, + type, + doi, + parentPublication: publication.id, + parentPublicationType: publication.type, + title: title || '', + createdBy, + authorFirstName: user.firstName, + authorLastName: user.lastName || '', + currentStatus, + publishedDate: publishedDate?.toISOString() || '', + authors: [] + }; + }); + + const publicationIds = linkedTo.map((link) => link.id).concat(linkedFrom.map((link) => link.id)); + + // get coAuthors for each latest LIVE version of each publication + const versions = await client.prisma.publicationVersion.findMany({ + where: { + isLatestLiveVersion: true, + versionOf: { + in: publicationIds + } + }, + select: { + versionOf: true, + coAuthors: { + select: { + id: true, + linkedUser: true, + user: { + select: { + orcid: true, + firstName: true, + lastName: true + } + } + }, + orderBy: { + position: 'asc' + } + } + } + }); + + // add authors to 'linkedTo' publications + linkedTo.forEach((link) => { + const authors = versions.find((version) => version.versionOf === link.id)?.coAuthors || []; + + Object.assign(link, { + authors + }); + }); + + // add authors to 'linkedFrom' publications + linkedFrom.forEach((link) => { + const authors = versions.find((version) => version.versionOf === link.id)?.coAuthors || []; + + Object.assign(link, { + authors + }); + }); + + return { + publication: { + id: publication.id, + type: publication.type, + doi: publication.doi, title: latestLiveVersion.title || '', createdBy: latestLiveVersion.createdBy, currentStatus: latestLiveVersion.currentStatus, @@ -1269,10 +852,16 @@ export const getLinksForPublication = async (id: string): Promise => { - const references = await referenceService.getAllByPublicationVersion(publication.versions[0].id); - const htmlTemplate = Helpers.createPublicationHTMLTemplate(publication, references); +export const generatePDF = async (publicationVersion: I.PublicationVersion): Promise => { + const references = await referenceService.getAllByPublicationVersion(publicationVersion.id); + const { linkedTo } = await getDirectLinksForPublication(publicationVersion.versionOf); + const htmlTemplate = Helpers.createPublicationHTMLTemplate(publicationVersion, references, linkedTo); const isLocal = process.env.STAGE === 'local'; let browser: Browser | null = null; @@ -1299,23 +888,23 @@ export const generatePDF = async (publication: I.PublicationWithVersionAttached) preferCSSPageSize: true, printBackground: true, displayHeaderFooter: true, - headerTemplate: Helpers.createPublicationHeaderTemplate(publication), - footerTemplate: Helpers.createPublicationFooterTemplate(publication) + headerTemplate: Helpers.createPublicationHeaderTemplate(publicationVersion), + footerTemplate: Helpers.createPublicationFooterTemplate(publicationVersion) }); // upload pdf to S3 await s3.client.send( new PutObjectCommand({ Bucket: `science-octopus-publishing-pdfs-${process.env.STAGE}`, - Key: `${publication.id}.pdf`, + Key: `${publicationVersion.versionOf}.pdf`, ContentType: 'application/pdf', Body: pdf }) ); - console.log('Successfully generated PDF for publicationId: ', publication.id); + console.log('Successfully generated PDF for publicationId: ', publicationVersion.versionOf); - return `${s3.endpoint}/science-octopus-publishing-pdfs-${process.env.STAGE}/${publication.id}.pdf`; + return `${s3.endpoint}/science-octopus-publishing-pdfs-${process.env.STAGE}/${publicationVersion.versionOf}.pdf`; } catch (err) { console.error(err); @@ -1473,3 +1062,19 @@ export const updateTopics = async (id: string, topics: string[]) => { return updateTopics.topics; }; + +export const getPublicationTopics = (id: string) => + client.prisma.topic.findMany({ + where: { + publications: { + some: { + id + } + } + }, + select: { + id: true, + createdAt: true, + title: true + } + }); diff --git a/api/src/components/publicationVersion/__tests__/deletePublicationVersion.test.ts b/api/src/components/publicationVersion/__tests__/deletePublicationVersion.test.ts new file mode 100644 index 000000000..cf557eb4c --- /dev/null +++ b/api/src/components/publicationVersion/__tests__/deletePublicationVersion.test.ts @@ -0,0 +1,73 @@ +import * as testUtils from 'lib/testUtils'; +import * as client from 'lib/client'; + +describe('Delete publication versions', () => { + beforeEach(async () => { + await testUtils.clearDB(); + await testUtils.testSeed(); + }); + + test('User can delete their own DRAFT publication version', async () => { + const deletePublicationVersion = await testUtils.agent + .delete('/publication-versions/publication-problem-draft-v1') + .query({ + apiKey: '000000005' + }); + + expect(deletePublicationVersion.status).toEqual(200); + + const checkForPublicationVersion = await client.prisma.publication.count({ + where: { + id: 'publication-problem-draft' + } + }); + + expect(checkForPublicationVersion).toEqual(0); + }); + + test('User cannot delete their own LIVE publication version', async () => { + const deletePublicationVersion = await testUtils.agent + .delete('/publication-versions/publication-problem-live-v1') + .query({ + apiKey: '123456789' + }); + + expect(deletePublicationVersion.status).toEqual(403); + + const checkForPublicationVersion = await client.prisma.publication.count({ + where: { + id: 'publication-problem-live' + } + }); + + expect(checkForPublicationVersion).toEqual(1); + }); + + test('User cannot delete a DRAFT publication version they did not create', async () => { + const deletePublicationVersion = await testUtils.agent + .delete('/publication-versions/publication-problem-draft-v1') + .query({ + apiKey: '987654321' + }); + + expect(deletePublicationVersion.status).toEqual(403); + }); + + test('User cannot delete a LIVE publication version they did not create', async () => { + const deletePublicationVersion = await testUtils.agent + .delete('/publication-versions/publication-problem-live-v1') + .query({ + apiKey: '987654321' + }); + + expect(deletePublicationVersion.status).toEqual(403); + }); + + test('Unauthenticated user cannot delete a DRAFT publication version they did not create', async () => { + const deletePublicationVersion = await testUtils.agent.delete( + '/publication-versions/publication-problem-draft-v1' + ); + + expect(deletePublicationVersion.status).toEqual(401); + }); +}); diff --git a/api/src/components/publication/__tests__/updatePublication.test.ts b/api/src/components/publicationVersion/__tests__/updatePublicationVersion.test.ts similarity index 57% rename from api/src/components/publication/__tests__/updatePublication.test.ts rename to api/src/components/publicationVersion/__tests__/updatePublicationVersion.test.ts index e152cbe61..6d13c5768 100644 --- a/api/src/components/publication/__tests__/updatePublication.test.ts +++ b/api/src/components/publicationVersion/__tests__/updatePublicationVersion.test.ts @@ -5,130 +5,130 @@ beforeEach(async () => { await testUtils.testSeed(); }); -describe('Update publication', () => { +describe('Update publication version', () => { test('Cannot update without permission', async () => { - const updatePublication = await testUtils.agent.patch('/publications/publication-interpretation-draft'); + const updatedVersion = await testUtils.agent.patch('/publication-versions/publication-interpretation-draft-v1'); - expect(updatePublication.status).toEqual(401); + expect(updatedVersion.status).toEqual(401); }); test('Cannot update with incorrect permissions', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 987654321 }); - expect(updatePublication.status).toEqual(403); + expect(updatedVersion.status).toEqual(403); }); - test('Can update publication title', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + test('Can update publication version title', async () => { + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 123456789 }) .send({ title: 'New title' }); - expect(updatePublication.status).toEqual(200); - expect(updatePublication.body.title).toEqual('New title'); + expect(updatedVersion.status).toEqual(200); + expect(updatedVersion.body.title).toEqual('New title'); }); - test('Can update publication content if "safe" HTML', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + test('Can update publication version content if "safe" HTML', async () => { + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 123456789 }) .send({ content: '

Hello Nathan

' }); - expect(updatePublication.status).toEqual(200); + expect(updatedVersion.status).toEqual(200); }); test('HTML is sanitised if not "safe" (1)', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 123456789 }) .send({ content: '

Hello Nathan

' }); - expect(updatePublication.body.content).toEqual('

Hello Nathan

'); + expect(updatedVersion.body.content).toEqual('

Hello Nathan

'); }); test('HTML is sanitised if not "safe" (2)', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 123456789 }) .send({ content: '

Hello Nathan

' }); - expect(updatePublication.body.content).toEqual('

Hello Nathan

'); + expect(updatedVersion.body.content).toEqual('

Hello Nathan

'); }); - test('Cannot update publication licence', async () => { + test('Cannot update publication version licence', async () => { // This was previously possible but we have now removed the ability because // there is only one licence type we want people to use and we set it automatically. - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 123456789 }) .send({ licence: 'CC_BY_SA' }); - expect(updatePublication.status).toEqual(422); + expect(updatedVersion.status).toEqual(422); }); test('Can update keywords', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 123456789 }) .send({ keywords: ['science', 'technology'] }); - expect(updatePublication.body.keywords.length).toEqual(2); + expect(updatedVersion.body.keywords.length).toEqual(2); }); test('Can update description', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 123456789 }) .send({ description: 'Test description' }); - expect(updatePublication.body.description).toEqual('Test description'); + expect(updatedVersion.body.description).toEqual('Test description'); }); - test('Cannot update publication with invalid update parameter', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + test('Cannot update publication version with invalid update parameter', async () => { + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 123456789 }) .send({ doesNotExist: 'invalid-parameter' }); - expect(updatePublication.status).toEqual(422); + expect(updatedVersion.status).toEqual(422); }); test('Cannot update LIVE publication', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-real-world-application-live') + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-real-world-application-live-v1') .query({ apiKey: 123456789 }) .send({ title: 'Brand new title' }); - expect(updatePublication.status).toEqual(404); + expect(updatedVersion.status).toEqual(404); }); test('Cannot add more than 10 keywords', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 123456789 }) .send({ keywords: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] }); - expect(updatePublication.status).toEqual(422); + expect(updatedVersion.status).toEqual(422); }); test('Cannot add more than 160 characters into a description', async () => { - const updatePublication = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + const updatedVersion = await testUtils.agent + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: 123456789 }) .send({ description: 'testing123testing123testing123testing123testing123testing123testing123testing123testing123testing123testing123testing123testing123testing123testing123testing123x' }); - expect(updatePublication.status).toEqual(422); + expect(updatedVersion.status).toEqual(422); }); // Language tests test('Valid publication updated by real user when provided a correct ISO-639-1 language code', async () => { const createPublicationRequest = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: '123456789' }) @@ -142,7 +142,7 @@ describe('Update publication', () => { test('Publication failed to be updated if language code provided is not out of the ISO-639-1 language list', async () => { const createPublicationRequest = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: '123456789' }) @@ -155,7 +155,7 @@ describe('Update publication', () => { test('Publication failed to be updated if language provided is less than 2 chars', async () => { const createPublicationRequest = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: '123456789' }) @@ -168,7 +168,7 @@ describe('Update publication', () => { test('Publication failed to be updated if language provided is more than 2 chars', async () => { const createPublicationRequest = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: '123456789' }) @@ -181,7 +181,7 @@ describe('Update publication', () => { test('Publication failed to update if is not protocol or hypotheses and supplies a self declaration', async () => { const createPublicationRequest = await testUtils.agent - .patch('/publications/publication-interpretation-draft') + .patch('/publication-versions/publication-interpretation-draft-v1') .query({ apiKey: '123456789' }) diff --git a/api/src/components/publicationVersion/__tests__/updateStatus.test.ts b/api/src/components/publicationVersion/__tests__/updateStatus.test.ts new file mode 100644 index 000000000..13218d6b6 --- /dev/null +++ b/api/src/components/publicationVersion/__tests__/updateStatus.test.ts @@ -0,0 +1,257 @@ +import * as testUtils from 'lib/testUtils'; + +beforeEach(async () => { + await testUtils.clearDB(); + await testUtils.testSeed(); +}); + +describe('Update publication version status', () => { + test('User with permissions can update their publication version to LIVE from DRAFT (after creating a link)', async () => { + const updatePublicationVersionAttemptOne = await testUtils.agent + .put('/publication-versions/publication-analysis-draft-v1/status/LIVE') + .query({ + apiKey: '123456789' + }); + + expect(updatePublicationVersionAttemptOne.status).toEqual(403); + + // add a valid link + await testUtils.agent + .post('/links') + .query({ + apiKey: '123456789' + }) + .send({ + from: 'publication-analysis-draft', + to: 'publication-data-live' + }); + + const updatePublicationVersionAttemptTwo = await testUtils.agent + .put('/publication-versions/publication-analysis-draft-v1/status/LIVE') + .query({ + apiKey: '123456789' + }); + + expect(updatePublicationVersionAttemptTwo.status).toEqual(200); + }); + + test('User with permissions can update their publication version to LIVE from DRAFT', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-hypothesis-draft-problem-live-v1/status/LIVE') + .query({ + apiKey: '123456789' + }); + + expect(updatePublicationVersion.status).toEqual(200); + }); + + test('User with permissions cannot update their publication to DRAFT from LIVE', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-problem-live-v1/status/DRAFT') + .query({ + apiKey: '123456789' + }); + + expect(updatePublicationVersion.status).toEqual(403); + }); + + test('User without permissions cannot update their publication to LIVE from DRAFT', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-hypothesis-draft-problem-live-v1/status/LIVE') + .query({ + apiKey: '987654321' + }); + + expect(updatePublicationVersion.status).toEqual(403); + }); + + test('User with permissions cannot update their publication to LIVE from DRAFT if there is no content.', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-problem-draft-no-content-v1/status/LIVE') + .query({ + apiKey: '123456789' + }); + + expect(updatePublicationVersion.status).toEqual(403); + }); + + test('User with permissions cannot update their publication to LIVE from DRAFT if there is no licence.', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-hypothesis-draft-v1/status/LIVE') + .query({ + apiKey: '000000005' + }); + + expect(updatePublicationVersion.status).toEqual(403); + }); + + test('User with permissions can update their publication version to LIVE from DRAFT and a publishedDate is created', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-hypothesis-draft-problem-live-v1/status/LIVE') + .query({ + apiKey: '123456789' + }); + + expect(updatePublicationVersion.status).toEqual(200); + expect(updatePublicationVersion.body.message).toEqual('Publication is now LIVE.'); + }); + + // COI tests + test('User with permissions cannot update their publication to LIVE if they have a conflict of interest, but have not provided coi text', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-problem-draft-with-coi-but-no-text-v1/status/LIVE') + .query({ + apiKey: '123456789' + }); + + expect(updatePublicationVersion.status).toEqual(403); + }); + + test('User with permissions can update their publication version to LIVE with a conflict of interest, if they have provided text', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-problem-draft-with-coi-with-text-v1/status/LIVE') + .query({ + apiKey: '123456789' + }); + + expect(updatePublicationVersion.status).toEqual(200); + }); + + test('User with permissions can update their publication version to LIVE if they have no conflict of interest & have not provided text', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-problem-draft-with-no-coi-with-no-text-v1/status/LIVE') + .query({ + apiKey: '123456789' + }); + + expect(updatePublicationVersion.status).toEqual(200); + }); + + test('User with permissions can update their publication version to LIVE if they have no conflict of interest & have provided text', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-problem-draft-with-no-coi-with-text-v1/status/LIVE') + .query({ + apiKey: '123456789' + }); + + expect(updatePublicationVersion.status).toEqual(200); + }); + + test('Publication owner can publish if all co-authors are confirmed', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-protocol-draft-v1/status/LIVE') + .query({ + apiKey: '000000005' + }); + + expect(updatePublicationVersion.status).toEqual(200); + + expect(updatePublicationVersion.body.message).toEqual('Publication is now LIVE.'); + }); + + test('Publication owner cannot publish if not all co-authors are confirmed', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-hypothesis-draft-v1/status/LIVE') + .query({ + apiKey: '000000005' + }); + + expect(updatePublicationVersion.status).toEqual(403); + expect(updatePublicationVersion.body.message).toEqual( + 'Publication is not ready to be made LIVE. Make sure all fields are filled in.' + ); + + const getPublicationVersion = await testUtils.agent + .get('/publications/publication-hypothesis-draft/publication-versions/latest') + .query({ + apiKey: '000000005' + }); + + expect(getPublicationVersion.body.currentStatus).toEqual('DRAFT'); + }); + + test('User other than the owner (does not have permission) cannot publish if co-authors all approved', async () => { + const updatePublicationVersion = await testUtils.agent + .put('/publication-versions/publication-hypothesis-draft-v1/status/LIVE') + .query({ + apiKey: '000000006' + }); + + expect(updatePublicationVersion.status).toEqual(403); + expect(updatePublicationVersion.body.message).toEqual( + 'You do not have permission to modify the status of this publication.' + ); + + const getPublicationVersion = await testUtils.agent + .get('/publications/publication-hypothesis-draft/publication-versions/latest') + .query({ + apiKey: '000000005' + }); + + expect(getPublicationVersion.body.currentStatus).toEqual('DRAFT'); + }); + + test('Publication owner cannot update publication status to LOCKED if there are no co-authors', async () => { + const response = await testUtils.agent.put('/publication-versions/publication-2-v1/status/LOCKED').query({ + apiKey: '987654321' + }); + + expect(response.status).toEqual(403); + expect(response.body.message).toEqual( + 'Publication is not ready to be LOCKED. Make sure all fields are filled in.' + ); + }); + + test('Throws an error if trying to update publication status to the same status', async () => { + const response = await testUtils.agent.put('/publication-versions/publication-2-v1/status/DRAFT').query({ + apiKey: '987654321' + }); + + expect(response.status).toEqual(403); + expect(response.body.message).toEqual('Publication status is already DRAFT.'); + }); + + test('Publication status can be updated from DRAFT to LOCKED only after requesting approvals', async () => { + // try to update status to LOCKED + const updateStatusResponse1 = await testUtils.agent + .put('/publication-versions/publication-problem-draft-v1/status/LOCKED') + .query({ + apiKey: '000000005' + }); + + expect(updateStatusResponse1.status).toEqual(403); + expect(updateStatusResponse1.body.message).toEqual( + 'Publication is not ready to be LOCKED. Make sure all fields are filled in.' + ); + + // request co-authors approvals + const requestApprovalsResponse = await testUtils.agent + .put('/publication-versions/publication-problem-draft-v1/coauthors/request-approval') + .query({ + apiKey: '000000005' + }); + + expect(requestApprovalsResponse.status).toEqual(200); + + // try to update status to LOCKED again + const updateStatusResponse2 = await testUtils.agent + .put('/publication-versions/publication-problem-draft-v1/status/LOCKED') + .query({ + apiKey: '000000005' + }); + + expect(updateStatusResponse2.status).toEqual(200); + expect(updateStatusResponse2.body.message).toEqual('Publication status updated to LOCKED.'); + }); + + test('Publication status can be updated from LOCKED to LIVE after all co-authors approved', async () => { + const response = await testUtils.agent + .put('/publication-versions/locked-publication-problem-confirmed-co-authors-v1/status/LIVE') + .query({ + apiKey: '123456789' + }); + + expect(response.status).toEqual(200); + expect(response.body.message).toEqual('Publication is now LIVE.'); + }); +}); diff --git a/api/src/components/publicationVersion/controller.ts b/api/src/components/publicationVersion/controller.ts new file mode 100644 index 000000000..e157801b0 --- /dev/null +++ b/api/src/components/publicationVersion/controller.ts @@ -0,0 +1,303 @@ +import htmlToText from 'html-to-text'; +import * as I from 'interface'; +import * as response from 'lib/response'; +import * as publicationVersionService from 'publicationVersion/service'; +import * as publicationService from 'publication/service'; +import * as coAuthorService from 'coauthor/service'; +import * as referenceService from 'reference/service'; +import * as helpers from 'lib/helpers'; +import * as sqs from 'lib/sqs'; + +export const get = async ( + event: I.APIRequest +): Promise => { + const { id, version } = event.pathParameters; + + try { + const publicationVersion = await publicationVersionService.get(id, version); + + if (!publicationVersion) { + return response.json(404, { + message: 'Publication version not found.' + }); + } + + // anyone can see a LIVE publication + if (publicationVersion.currentStatus === 'LIVE') { + return response.json(200, publicationVersion); + } + + // only the owner or co-authors can view publications + if ( + event.user?.id === publicationVersion.user.id || + publicationVersion.coAuthors.some((coAuthor) => coAuthor.linkedUser === event.user?.id) + ) { + return response.json(200, publicationVersion); + } + + return response.json(404, { + message: + 'Publication version is either not found, or you do not have permissions to view it in its current state.' + }); + } catch (err) { + console.log(err); + + return response.json(500, { message: 'Unknown server error.' }); + } +}; + +export const getAll = async ( + event: I.AuthenticatedAPIRequest +): Promise => { + try { + const openSearchPublications = await publicationService.getOpenSearchPublications(event.queryStringParameters); + + const publicationIds: string[] = openSearchPublications.body.hits.hits.map((hit) => hit._id as string); + + const publicationVersions = await publicationVersionService.getAllByPublicationIds(publicationIds); + + const versionsOrderedBySearch = publicationIds.map((publicationId) => + publicationVersions.find((version) => version.versionOf === publicationId) + ); + + return response.json(200, { + data: versionsOrderedBySearch, + metadata: { + total: openSearchPublications.body.hits.total.value, + limit: Number(event.queryStringParameters.limit) || 10, + offset: Number(event.queryStringParameters.offset) || 0 + } + }); + } catch (err) { + console.log(err); + + return response.json(500, { message: 'Unknown server error.' }); + } +}; + +export const update = async ( + event: I.AuthenticatedAPIRequest< + I.UpdatePublicationVersionRequestBody, + undefined, + I.UpdatePublicationVersionPathParams + > +): Promise => { + try { + const publicationVersion = await publicationVersionService.getById(event.pathParameters.id); + + if (!publicationVersion) { + return response.json(403, { + message: 'This publication version does not exist.' + }); + } + + if (publicationVersion.user.id !== event.user.id) { + return response.json(403, { + message: 'You do not have permission to modify this publication version.' + }); + } + + if (publicationVersion.currentStatus !== 'DRAFT') { + return response.json(404, { + message: 'A publication version that is not in DRAFT state cannot be updated.' + }); + } + + if (event.body.content) { + event.body.content = helpers.getSafeHTML(event.body.content); + } + + if ( + event.body.selfDeclaration !== undefined && + publicationVersion.publication.type !== 'PROTOCOL' && + publicationVersion.publication.type !== 'HYPOTHESIS' + ) { + return response.json(400, { + message: + 'You can not declare a self declaration for a publication that is not a protocol or hypothesis.' + }); + } + + if (event.body.dataAccessStatement !== undefined && publicationVersion.publication.type !== 'DATA') { + return response.json(400, { + message: 'You can not supply a data access statement on a non data publication.' + }); + } + + if (event.body.dataPermissionsStatement !== undefined && publicationVersion.publication.type !== 'DATA') { + return response.json(400, { + message: 'You can not supply a data permissions statement on a non data publication.' + }); + } + + await publicationVersionService.update(event.pathParameters.id, event.body); + + const updatedPublicationVersion = await publicationVersionService.getById(event.pathParameters.id); + + return response.json(200, updatedPublicationVersion); + } catch (err) { + console.log(err); + + return response.json(500, { message: 'Unknown server error.' }); + } +}; + +export const updateStatus = async ( + event: I.AuthenticatedAPIRequest +): Promise => { + try { + const publicationVersion = await publicationVersionService.getById(event.pathParameters.id); + + if (!publicationVersion) { + return response.json(404, { + message: 'This publication version does not exist.' + }); + } + + if (publicationVersion.createdBy !== event.user.id) { + return response.json(403, { + message: 'You do not have permission to modify the status of this publication.' + }); + } + + const newStatus = event.pathParameters?.status; + const currentStatus = publicationVersion.currentStatus; + + if (currentStatus === 'LIVE') { + return response.json(403, { + message: 'A status of a publication that is not in DRAFT or LOCKED cannot be changed.' + }); + } + + if (currentStatus === newStatus) { + return response.json(403, { message: `Publication status is already ${newStatus}.` }); + } + + if (currentStatus === 'DRAFT') { + if (newStatus === 'LOCKED') { + // check if publication version actually has co-authors + if (publicationVersion.coAuthors.length === 1) { + return response.json(403, { message: 'Publication cannot be LOCKED without co-authors.' }); + } + + // check if publication version is ready to be LOCKED + if (!(await publicationVersionService.checkIsReadyToLock(publicationVersion))) { + return response.json(403, { + message: 'Publication is not ready to be LOCKED. Make sure all fields are filled in.' + }); + } + + // Lock publication from editing + await publicationVersionService.updateStatus(publicationVersion.id, 'LOCKED'); + + return response.json(200, { message: 'Publication status updated to LOCKED.' }); + } + + if (newStatus === 'LIVE') { + const isReadyToPublish = await publicationVersionService.checkIsReadyToPublish(publicationVersion); + + if (!isReadyToPublish) { + return response.json(403, { + message: 'Publication is not ready to be made LIVE. Make sure all fields are filled in.' + }); + } + } + } + + if (currentStatus === 'LOCKED') { + if (newStatus === 'DRAFT') { + // Update status to 'DRAFT' + await publicationVersionService.updateStatus(publicationVersion.id, newStatus); + + // Cancel co author approvals + await coAuthorService.resetCoAuthors(publicationVersion.id); + + return response.json(200, { + message: 'Publication unlocked for editing' + }); + } + + if (newStatus === 'LIVE') { + const isReadyToPublish = await publicationVersionService.checkIsReadyToPublish(publicationVersion); + + if (!isReadyToPublish) { + return response.json(403, { + message: 'Publication is not ready to be made LIVE. Make sure all fields are filled in.' + }); + } + } + } + + const updatedVersion = await publicationVersionService.updateStatus(publicationVersion.id, newStatus); + + // now that the publication version is LIVE, add/update the opensearch record + await publicationService.createOpenSearchRecord({ + id: updatedVersion.versionOf, + type: updatedVersion.publication.type, + title: updatedVersion.title, + licence: updatedVersion.licence, + description: updatedVersion.description, + keywords: updatedVersion.keywords, + content: updatedVersion.content, + publishedDate: updatedVersion.publishedDate, + cleanContent: htmlToText.convert(updatedVersion.content) + }); + + const references = await referenceService.getAllByPublicationVersion(updatedVersion.id); + const { linkedTo } = await publicationService.getDirectLinksForPublication(publicationVersion.versionOf, true); + + // Publication version is live, so update the DOI + await helpers.updateDOI(publicationVersion.publication.doi, publicationVersion, linkedTo, references); + + // send message to the pdf generation queue + // currently only on deployed instances while a local solution is developed + if (process.env.STAGE !== 'local') await sqs.sendMessage(publicationVersion.versionOf); + + return response.json(200, { message: 'Publication is now LIVE.' }); + } catch (err) { + console.log(err); + + return response.json(500, { message: 'Unknown server error.' }); + } +}; + +export const deleteVersion = async ( + event: I.AuthenticatedAPIRequest +): Promise => { + try { + const publicationVersion = await publicationVersionService.getById(event.pathParameters.id); + + if (!publicationVersion) { + return response.json(403, { + message: 'This publication version does not exist.' + }); + } + + if (publicationVersion.user.id !== event.user.id) { + return response.json(403, { + message: 'You do not have permission to delete this publication.' + }); + } + + // The logic here is a bit odd, but the currentStatus and publicationStatus array are not intrinsically linked + // so to be safe, we are checking that the current status is DRAFT and that the entire history of the publication + // has only ever been draft. + if ( + publicationVersion.currentStatus !== 'DRAFT' || + (publicationVersion.publicationStatus && + !publicationVersion.publicationStatus.every((status) => status.status !== 'LIVE')) + ) { + return response.json(403, { + message: 'A publication can only be deleted if it is currently a draft and has never been LIVE.' + }); + } + + await publicationVersionService.deleteVersion(publicationVersion); + + return response.json(200, { message: `Publication version ${event.pathParameters.id} has been deleted` }); + } catch (err) { + console.log(err); + + return response.json(500, { message: 'Unknown server error.' }); + } +}; diff --git a/api/src/components/publicationVersion/routes.ts b/api/src/components/publicationVersion/routes.ts new file mode 100644 index 000000000..1414dcb58 --- /dev/null +++ b/api/src/components/publicationVersion/routes.ts @@ -0,0 +1,32 @@ +import middy from '@middy/core'; + +import * as middleware from 'middleware'; +import * as publicationVersionController from './controller'; +import * as publicationVersionSchema from './schema'; + +export const get = middy(publicationVersionController.get) + .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) + .use(middleware.httpJsonBodyParser()) + .use(middleware.authentication(true)); + +export const getAll = middy(publicationVersionController.getAll) + .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) + .use(middleware.httpJsonBodyParser()) + .use(middleware.validator(publicationVersionSchema.getAll, 'queryStringParameters')); + +export const update = middy(publicationVersionController.update) + .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) + .use(middleware.httpJsonBodyParser()) + .use(middleware.authentication()) + .use(middleware.validator(publicationVersionSchema.update, 'body')); + +export const updateStatus = middy(publicationVersionController.updateStatus) + .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) + .use(middleware.httpJsonBodyParser()) + .use(middleware.authentication()) + .use(middleware.validator(publicationVersionSchema.updateStatus, 'pathParameters')); + +export const deleteVersion = middy(publicationVersionController.deleteVersion) + .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) + .use(middleware.httpJsonBodyParser()) + .use(middleware.authentication()); diff --git a/api/src/components/publication/schema/getAll.ts b/api/src/components/publicationVersion/schema/getAll.ts similarity index 92% rename from api/src/components/publication/schema/getAll.ts rename to api/src/components/publicationVersion/schema/getAll.ts index 0c5d3f9d7..663fcad4f 100644 --- a/api/src/components/publication/schema/getAll.ts +++ b/api/src/components/publicationVersion/schema/getAll.ts @@ -1,6 +1,6 @@ import * as I from 'interface'; -const getAllSchema: I.JSONSchemaType = { +const getAll: I.JSONSchemaType = { type: 'object', properties: { type: { @@ -48,4 +48,4 @@ const getAllSchema: I.JSONSchemaType = { additionalProperties: false }; -export default getAllSchema; +export default getAll; diff --git a/api/src/components/publicationVersion/schema/index.ts b/api/src/components/publicationVersion/schema/index.ts new file mode 100644 index 000000000..9cc369163 --- /dev/null +++ b/api/src/components/publicationVersion/schema/index.ts @@ -0,0 +1,3 @@ +export { default as getAll } from './getAll'; +export { default as update } from './update'; +export { default as updateStatus } from './updateStatus'; diff --git a/api/src/components/publication/schema/update.ts b/api/src/components/publicationVersion/schema/update.ts similarity index 93% rename from api/src/components/publication/schema/update.ts rename to api/src/components/publicationVersion/schema/update.ts index 1baff73d7..9e273c65b 100644 --- a/api/src/components/publication/schema/update.ts +++ b/api/src/components/publicationVersion/schema/update.ts @@ -1,7 +1,7 @@ import * as I from 'interface'; import * as H from 'lib/helpers'; -const updatePublicationSchema: I.Schema = { +const updatePublicationVersionSchema: I.Schema = { type: 'object', properties: { title: { @@ -61,4 +61,4 @@ const updatePublicationSchema: I.Schema = { additionalProperties: false }; -export default updatePublicationSchema; +export default updatePublicationVersionSchema; diff --git a/api/src/components/publication/schema/updateStatus.ts b/api/src/components/publicationVersion/schema/updateStatus.ts similarity index 79% rename from api/src/components/publication/schema/updateStatus.ts rename to api/src/components/publicationVersion/schema/updateStatus.ts index afd14fb5b..62af1f796 100644 --- a/api/src/components/publication/schema/updateStatus.ts +++ b/api/src/components/publicationVersion/schema/updateStatus.ts @@ -1,6 +1,6 @@ import * as I from 'interface'; -const updatedPublicationSchema: I.Schema = { +const updateStatusSchema: I.Schema = { type: 'object', properties: { status: { @@ -15,4 +15,4 @@ const updatedPublicationSchema: I.Schema = { additionalProperties: false }; -export default updatedPublicationSchema; +export default updateStatusSchema; diff --git a/api/src/components/publicationVersion/service.ts b/api/src/components/publicationVersion/service.ts index ed3ecc7d4..c17f2714a 100644 --- a/api/src/components/publicationVersion/service.ts +++ b/api/src/components/publicationVersion/service.ts @@ -1,9 +1,11 @@ import { Prisma } from '@prisma/client'; import * as I from 'interface'; import * as client from 'lib/client'; +import * as publicationService from 'publication/service'; +import * as Helpers from 'lib/helpers'; -export const get = async (id: string) => { - const publicationVersion = client.prisma.publicationVersion.findFirst({ +export const getById = (id: string) => + client.prisma.publicationVersion.findFirst({ where: { id }, @@ -13,9 +15,7 @@ export const get = async (id: string) => { id: true, type: true, doi: true, - linkedTo: true, - linkedFrom: true, - topics: true + url_slug: true } }, publicationStatus: { @@ -76,9 +76,147 @@ export const get = async (id: string) => { } }); - return publicationVersion; +export const get = (publicationId: string, version: string | number) => + client.prisma.publicationVersion.findFirst({ + where: { + versionOf: publicationId, + ...(typeof version === 'number' || Number(version) + ? { versionNumber: Number(version) } + : version === 'latest' + ? { isLatestVersion: true } + : version === 'latestLive' + ? { + isLatestLiveVersion: true + } + : { id: version }) + }, + include: { + publication: { + select: { + id: true, + type: true, + doi: true, + url_slug: true + } + }, + publicationStatus: { + select: { + status: true, + createdAt: true, + id: true + }, + orderBy: { + createdAt: 'desc' + } + }, + funders: { + select: { + id: true, + city: true, + country: true, + name: true, + link: true, + ror: true + } + }, + coAuthors: { + select: { + id: true, + email: true, + linkedUser: true, + publicationVersionId: true, + confirmedCoAuthor: true, + approvalRequested: true, + createdAt: true, + reminderDate: true, + isIndependent: true, + affiliations: true, + user: { + select: { + firstName: true, + lastName: true, + orcid: true + } + } + }, + orderBy: { + position: 'asc' + } + }, + user: { + select: { + id: true, + orcid: true, + firstName: true, + lastName: true, + email: true, + createdAt: true, + updatedAt: true + } + } + } + }); + +export const getAllByPublicationIds = async (ids: string[]) => { + // Get latest versions of these publications + const latestVersions = await client.prisma.publicationVersion.findMany({ + where: { + versionOf: { + in: ids + }, + isLatestLiveVersion: true + }, + include: { + publication: { + select: { + id: true, + type: true, + doi: true, + url_slug: true + } + }, + user: { + select: { + firstName: true, + lastName: true, + id: true, + orcid: true + } + }, + coAuthors: { + select: { + id: true, + linkedUser: true, + user: { + select: { + orcid: true, + firstName: true, + lastName: true + } + } + }, + orderBy: { + position: 'asc' + } + } + } + }); + + if (ids.length !== latestVersions.length) { + throw Error('Unable to find all latest versions for all requested publications.'); + } + + return latestVersions; }; +export const update = (id: string, updateContent: I.UpdatePublicationVersionRequestBody) => + client.prisma.publicationVersion.update({ + where: { + id + }, + data: updateContent + }); + export const updateStatus = async (id: string, status: I.PublicationStatusEnum) => { const query = { where: { @@ -136,3 +274,133 @@ export const validateConflictOfInterest = (version: I.PublicationVersion) => { return true; }; + +export const checkIsReadyToPublish = async (publicationVersion: I.PublicationVersion): Promise => { + if (!publicationVersion) { + return false; + } + + const { linkedTo } = await publicationService.getDirectLinksForPublication(publicationVersion.versionOf, true); + const topics = await publicationService.getPublicationTopics(publicationVersion.versionOf); + + const hasAtLeastOneLinkOrTopic = + linkedTo.length !== 0 || (publicationVersion.publication.type === 'PROBLEM' && topics.length !== 0); + const hasFilledRequiredFields = + ['title', 'licence'].every((field) => publicationVersion[field]) && + !Helpers.isEmptyContent(publicationVersion.content || ''); + const conflictOfInterest = validateConflictOfInterest(publicationVersion); + const hasPublishDate = Boolean(publicationVersion.publishedDate); + const isDataAndHasEthicalStatement = + publicationVersion.publication.type === 'DATA' ? publicationVersion.ethicalStatement !== null : true; + const isDataAndHasPermissionsStatement = + publicationVersion.publication.type === 'DATA' ? publicationVersion.dataPermissionsStatement !== null : true; + + const coAuthorsAreVerified = !!publicationVersion.coAuthors.every( + (coAuthor) => coAuthor.confirmedCoAuthor && (coAuthor.isIndependent || coAuthor.affiliations.length) + ); + + return ( + hasAtLeastOneLinkOrTopic && + hasFilledRequiredFields && + conflictOfInterest && + !hasPublishDate && + isDataAndHasEthicalStatement && + isDataAndHasPermissionsStatement && + coAuthorsAreVerified && + publicationVersion.isLatestVersion + ); +}; + +export const checkIsReadyToRequestApprovals = async (publicationVersion: I.PublicationVersion): Promise => { + if (!publicationVersion) { + return false; + } + + if (!publicationVersion.isLatestVersion || publicationVersion.currentStatus !== 'DRAFT') { + return false; + } + + const { linkedTo } = await publicationService.getDirectLinksForPublication(publicationVersion.versionOf, true); + const topics = await publicationService.getPublicationTopics(publicationVersion.versionOf); + + const hasAtLeastOneLinkOrTopic = + linkedTo.length !== 0 || (publicationVersion.publication.type === 'PROBLEM' && topics.length !== 0); + const hasFilledRequiredFields = + ['title', 'licence'].every((field) => publicationVersion[field]) && + !Helpers.isEmptyContent(publicationVersion.content || ''); + const conflictOfInterest = validateConflictOfInterest(publicationVersion); + const isDataAndHasEthicalStatement = + publicationVersion.publication.type === 'DATA' ? publicationVersion.ethicalStatement !== null : true; + const isDataAndHasPermissionsStatement = + publicationVersion.publication.type === 'DATA' ? publicationVersion.dataPermissionsStatement !== null : true; + const hasConfirmedAffiliations = !!publicationVersion.coAuthors.some( + (author) => + author.linkedUser === publicationVersion.createdBy && (author.isIndependent || author.affiliations.length) + ); + + return ( + hasAtLeastOneLinkOrTopic && + hasFilledRequiredFields && + conflictOfInterest && + isDataAndHasEthicalStatement && + isDataAndHasPermissionsStatement && + hasConfirmedAffiliations && + publicationVersion.isLatestVersion + ); +}; + +export const checkIsReadyToLock = async (publicationVersion: I.PublicationVersion): Promise => { + if (!publicationVersion) { + return false; + } + + if (publicationVersion.currentStatus !== 'DRAFT') { + return false; + } + + const isReadyToRequestApprovals = await checkIsReadyToRequestApprovals(publicationVersion); + const hasRequestedApprovals = !!publicationVersion.coAuthors.some((author) => author.approvalRequested); + + return isReadyToRequestApprovals && hasRequestedApprovals; +}; + +export const deleteVersion = async (publicationVersion: I.PublicationVersion) => { + if ( + publicationVersion.isLatestVersion && + publicationVersion.versionNumber === 1 && + publicationVersion.currentStatus !== 'LIVE' + ) { + // if there's only one DRAFT version and that's the latest one, we can safely delete the entire publication + await publicationService.deletePublication(publicationVersion.versionOf); + } else { + // delete this version + await client.prisma.publicationVersion.delete({ + where: { + id: publicationVersion.id + } + }); + + // get previous version + const previousVersion = await client.prisma.publicationVersion.findFirst({ + where: { + versionOf: publicationVersion.versionOf, + versionNumber: publicationVersion.versionNumber - 1 + }, + select: { + id: true + } + }); + + if (previousVersion) { + // make the previous version "isLatestVersion=true" + await client.prisma.publicationVersion.update({ + where: { + id: previousVersion.id + }, + data: { + isLatestVersion: true + } + }); + } + } +}; diff --git a/api/src/components/reference/__tests__/getReference.test.ts b/api/src/components/reference/__tests__/getReference.test.ts index 91adc909d..0e16740fb 100644 --- a/api/src/components/reference/__tests__/getReference.test.ts +++ b/api/src/components/reference/__tests__/getReference.test.ts @@ -8,7 +8,7 @@ describe('get references', () => { test('Any user (unauthenticated) can get the references for a live publication', async () => { const reference = await testUtils.agent.get( - '/publicationVersions/publication-real-world-application-live-v1/reference' + '/publication-versions/publication-real-world-application-live-v1/references' ); expect(reference.status).toEqual(200); diff --git a/api/src/components/reference/__tests__/updateAllReferences.test.ts b/api/src/components/reference/__tests__/updateAllReferences.test.ts index 44f21c285..a08d15c38 100644 --- a/api/src/components/reference/__tests__/updateAllReferences.test.ts +++ b/api/src/components/reference/__tests__/updateAllReferences.test.ts @@ -8,7 +8,7 @@ describe('update all references', () => { test('User can update all references from their own draft publication', async () => { const reference = await testUtils.agent - .put('/publicationVersions/publication-interpretation-draft-v1/reference') + .put('/publication-versions/publication-interpretation-draft-v1/references') .query({ apiKey: '123456789' }) .send([ { @@ -25,7 +25,7 @@ describe('update all references', () => { test('User must be the author of the publication to update the references', async () => { const reference = await testUtils.agent - .put('/publicationVersions/publication-interpretation-draft-v1/reference') + .put('/publication-versions/publication-interpretation-draft-v1/references') .query({ apiKey: '987654321' }) .send([ { @@ -42,7 +42,7 @@ describe('update all references', () => { test('The author can only update the references for a draft publication', async () => { const reference = await testUtils.agent - .put('/publicationVersions/publication-real-world-application-live-v1/reference') + .put('/publication-versions/publication-real-world-application-live-v1/references') .query({ apiKey: '123456789' }) .send([ { @@ -76,12 +76,12 @@ describe('update all references', () => { ]; const reference = await testUtils.agent - .put('/publicationVersions/publication-interpretation-draft-v1/reference') + .put('/publication-versions/publication-interpretation-draft-v1/references') .query({ apiKey: '123456789' }) .send(newReferencesArray); const checkReference = await testUtils.agent.get( - '/publicationVersions/publication-interpretation-draft-v1/reference' + '/publication-versions/publication-interpretation-draft-v1/references' ); expect(reference.body.count).toEqual(2); diff --git a/api/src/components/reference/controller.ts b/api/src/components/reference/controller.ts index 72a731a59..1e52ff9f0 100644 --- a/api/src/components/reference/controller.ts +++ b/api/src/components/reference/controller.ts @@ -19,7 +19,7 @@ export const updateAll = async ( event: I.AuthenticatedAPIRequest ): Promise => { try { - const version = await publicationVersionService.get(event.pathParameters.id); + const version = await publicationVersionService.getById(event.pathParameters.id); //check that the version exists if (!version) { diff --git a/api/src/components/user/__tests__/getUserPublications.test.ts b/api/src/components/user/__tests__/getUserPublications.test.ts index 58e633a4b..7d5c03241 100644 --- a/api/src/components/user/__tests__/getUserPublications.test.ts +++ b/api/src/components/user/__tests__/getUserPublications.test.ts @@ -1,41 +1,43 @@ import * as testUtils from 'lib/testUtils'; -describe('Get a given users publications', () => { - beforeEach(async () => { +describe('Get a given users publication versions', () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); - test('Current user can view publications including drafts', async () => { - const publications = await testUtils.agent - .get('/users/test-user-1/publications') + test('Current user can view publication versions including drafts', async () => { + const versions = await testUtils.agent + .get('/users/test-user-1/publication-versions') .query({ apiKey: 123456789, offset: 0, limit: 100 }); - expect(publications.status).toEqual(200); - expect(publications.body.results.length).toEqual(20); + expect(versions.status).toEqual(200); + expect(versions.body.results.length).toEqual(20); }); - test('Unauthenticated user can only view live publications', async () => { - const publications = await testUtils.agent.get('/users/test-user-1/publications'); + test('Unauthenticated user can only view live versions', async () => { + const versions = await testUtils.agent.get('/users/test-user-1/publication-versions'); - expect(publications.status).toEqual(200); - expect(publications.body.results.length).toEqual(8); + expect(versions.status).toEqual(200); + expect(versions.body.results.length).toEqual(8); }); - test('An authenticated user can only view live publications of another user', async () => { - const publications = await testUtils.agent.get('/users/test-user-1/publications').query({ apiKey: 987654321 }); + test('An authenticated user can only view live versions of another user', async () => { + const versions = await testUtils.agent + .get('/users/test-user-1/publication-versions') + .query({ apiKey: 987654321 }); - expect(publications.status).toEqual(200); - expect(publications.body.results.length).toEqual(8); + expect(versions.status).toEqual(200); + expect(versions.body.results.length).toEqual(8); }); test('Error message returned for a user that does not exist', async () => { - const publications = await testUtils.agent - .get('/users/user-does-not-exist/publications') + const versions = await testUtils.agent + .get('/users/user-does-not-exist/publication-versions') .query({ apiKey: 987654321 }); - expect(publications.body.results).toBe(undefined); - expect(publications.body.message).toBe('User not found'); - expect(publications.status).toEqual(400); + expect(versions.body.results).toBe(undefined); + expect(versions.body.message).toBe('User not found'); + expect(versions.status).toEqual(400); }); }); diff --git a/api/src/components/user/controller.ts b/api/src/components/user/controller.ts index 0d8da2236..6cd2e583e 100644 --- a/api/src/components/user/controller.ts +++ b/api/src/components/user/controller.ts @@ -35,8 +35,8 @@ export const get = async ( } }; -export const getPublications = async ( - event: I.OptionalAuthenticatedAPIRequest +export const getPublicationVersions = async ( + event: I.OptionalAuthenticatedAPIRequest ): Promise => { try { const isAccountOwner = Boolean(event.user?.id === event.pathParameters.id); @@ -47,13 +47,13 @@ export const getPublications = async ( return response.json(400, { message: 'User not found' }); } - const userPublications = await userService.getPublications( + const userPublicationVersions = await userService.getPublicationVersions( event.pathParameters.id, event.queryStringParameters, isAccountOwner ); - return response.json(200, userPublications); + return response.json(200, userPublicationVersions); } catch (err) { console.log(err); diff --git a/api/src/components/user/routes.ts b/api/src/components/user/routes.ts index baf8719b2..c62cf0df9 100644 --- a/api/src/components/user/routes.ts +++ b/api/src/components/user/routes.ts @@ -14,10 +14,10 @@ export const get = middy(userController.get) .use(middleware.httpJsonBodyParser()) .use(middleware.authentication(true)); -export const getPublications = middy(userController.getPublications) +export const getPublicationVersions = middy(userController.getPublicationVersions) .use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true })) .use(middleware.httpJsonBodyParser()) .use(middleware.authentication(true)) - .use(middleware.validator(userSchema.getPublications, 'queryStringParameters')); + .use(middleware.validator(userSchema.getPublicationVersions, 'queryStringParameters')); export const getUserList = userController.getUserList; diff --git a/api/src/components/user/schema/getPublications.ts b/api/src/components/user/schema/getPublicationVersions.ts similarity index 81% rename from api/src/components/user/schema/getPublications.ts rename to api/src/components/user/schema/getPublicationVersions.ts index e35d01fe1..7eb685750 100644 --- a/api/src/components/user/schema/getPublications.ts +++ b/api/src/components/user/schema/getPublicationVersions.ts @@ -1,6 +1,6 @@ import * as I from 'interface'; -const getPublicationsSchema: I.JSONSchemaType = { +const getPublicationVersionsSchema: I.JSONSchemaType = { type: 'object', properties: { offset: { @@ -27,4 +27,4 @@ const getPublicationsSchema: I.JSONSchemaType = { required: [] }; -export default getPublicationsSchema; +export default getPublicationVersionsSchema; diff --git a/api/src/components/user/schema/index.ts b/api/src/components/user/schema/index.ts index 3e40e4f77..b58962c7b 100644 --- a/api/src/components/user/schema/index.ts +++ b/api/src/components/user/schema/index.ts @@ -1,2 +1,2 @@ export { default as getAll } from './getAll'; -export { default as getPublications } from './getPublications'; +export { default as getPublicationVersions } from './getPublicationVersions'; diff --git a/api/src/components/user/service.ts b/api/src/components/user/service.ts index a77ded77d..9ff9404f7 100644 --- a/api/src/components/user/service.ts +++ b/api/src/components/user/service.ts @@ -132,88 +132,71 @@ export const get = async (id: string, isAccountOwner = false) => { return user; }; -export const getPublications = async (id: string, params: I.UserPublicationsFilters, isAccountOwner: boolean) => { +export const getPublicationVersions = async ( + id: string, + params: I.UserPublicationVersionsFilters, + isAccountOwner: boolean +) => { const { offset, limit, orderBy, orderDirection } = params; // Account owners can retrieve their DRAFT publications also const statuses: Array = isAccountOwner ? ['DRAFT', 'LIVE', 'LOCKED'] : ['LIVE']; - const where: Prisma.PublicationWhereInput = { + const where: Prisma.PublicationVersionWhereInput = { OR: [ { - versions: { - some: { - isLatestVersion: true, - createdBy: id - } - } + createdBy: id }, { - versions: { + coAuthors: { some: { - isLatestVersion: true, - coAuthors: { - some: { - linkedUser: id - } - } + linkedUser: id } } } ], - versions: { - some: { - isLatestVersion: true, - currentStatus: { - in: statuses - } - } - } + currentStatus: { + in: statuses + }, + ...(isAccountOwner ? { isLatestVersion: true } : { isLatestLiveVersion: true }) }; - const userPublications = await client.prisma.publication.findMany({ + const userPublicationVersions = await client.prisma.publicationVersion.findMany({ skip: offset, take: limit, where, - select: { - id: true, - type: true, - doi: true, - url_slug: true, - versions: { - where: { - isLatestVersion: true - }, + include: { + publication: { select: { - createdBy: true, - createdAt: true, - updatedAt: true, - title: true, - publishedDate: true, - currentStatus: true, - licence: true, - content: true, - coAuthors: { + id: true, + type: true, + doi: true, + url_slug: true + } + }, + user: { + select: { + firstName: true, + lastName: true, + id: true, + orcid: true + } + }, + coAuthors: { + select: { + id: true, + linkedUser: true, + confirmedCoAuthor: true, + user: { select: { - id: true, - approvalRequested: true, - confirmedCoAuthor: true, - code: true, - email: true, - publicationVersionId: true, - linkedUser: true, - user: { - select: { - orcid: true, - firstName: true, - lastName: true - } - } - }, - orderBy: { - position: 'asc' + orcid: true, + firstName: true, + lastName: true } } + }, + orderBy: { + position: 'asc' } } }, @@ -225,21 +208,9 @@ export const getPublications = async (id: string, params: I.UserPublicationsFilt : undefined }); - // Simplify publications - const simplifiedPublications = userPublications.map((publication) => { - const simplifiedPublication = { - ...publication, - ...publication.versions[0] - }; - // Discard versions field - const { versions, ...simplifiedPublicationRest } = simplifiedPublication; - - return simplifiedPublicationRest; - }); - - const totalUserPublications = await client.prisma.publication.count({ where }); + const totalUserPublications = await client.prisma.publicationVersion.count({ where }); - return { offset, limit, total: totalUserPublications, results: simplifiedPublications }; + return { offset, limit, total: totalUserPublications, results: userPublicationVersions }; }; export const getUserList = async () => { diff --git a/api/src/lib/helpers.ts b/api/src/lib/helpers.ts index 6f1dc598b..7e048deea 100644 --- a/api/src/lib/helpers.ts +++ b/api/src/lib/helpers.ts @@ -84,22 +84,21 @@ export const getFullDOIsStrings = (text: string): [] | RegExpMatchArray => export const updateDOI = async ( doi: string, - publication: I.PublicationWithVersionAttached, + publicationVersion: I.PublicationVersion, + linkedTo: I.LinkedToPublication[], references: I.Reference[] ): Promise => { - if (!publication) { + if (!publicationVersion) { throw Error('Publication not found'); } - const currentVersion = publication.versions[0]; - - if (!currentVersion.isLatestVersion) { + if (!publicationVersion.isLatestVersion) { throw Error('Supplied version is not current'); } const creators: I.DataCiteCreator[] = []; - currentVersion.coAuthors.forEach((author) => { + publicationVersion.coAuthors.forEach((author) => { if (author.user) { creators.push( createCreatorObject({ @@ -112,10 +111,10 @@ export const updateDOI = async ( } }); - const linkedPublications = publication.linkedTo.map((relatedIdentifier) => ({ - relatedIdentifier: relatedIdentifier.publicationToRef.doi, + const linkedPublications = linkedTo.map((link) => ({ + relatedIdentifier: link.doi, relatedIdentifierType: 'DOI', - relationType: relatedIdentifier.publicationToRef.type === 'PEER_REVIEW' ? 'Reviews' : 'Continues' + relationType: link.type === 'PEER_REVIEW' ? 'Reviews' : 'Continues' })); const doiReferences = references.map((reference) => { @@ -157,13 +156,13 @@ export const updateDOI = async ( }); // check if the creator of this version of the publication is not listed as an author - if (!currentVersion.coAuthors.find((author) => author.linkedUser === currentVersion.createdBy)) { + if (!publicationVersion.coAuthors.find((author) => author.linkedUser === publicationVersion.createdBy)) { // add creator to authors list as first author creators?.unshift( createCreatorObject({ - firstName: currentVersion.user.firstName, - lastName: currentVersion.user.lastName, - orcid: currentVersion.user.orcid, + firstName: publicationVersion.user.firstName, + lastName: publicationVersion.user.lastName, + orcid: publicationVersion.user.orcid, affiliations: [] }) ); @@ -174,7 +173,7 @@ export const updateDOI = async ( types: 'doi', attributes: { event: 'publish', - url: `${process.env.BASE_URL}/publications/${publication.id}`, + url: `${process.env.BASE_URL}/publications/${publicationVersion.versionOf}`, doi: doi, identifiers: [ { @@ -185,22 +184,22 @@ export const updateDOI = async ( creators, titles: [ { - title: currentVersion.title, + title: publicationVersion.title, lang: 'en' } ], publisher: 'Octopus', - publicationYear: currentVersion.createdAt.getFullYear(), + publicationYear: publicationVersion.createdAt.getFullYear(), contributors: [ { - name: `${currentVersion.user.lastName} ${currentVersion.user.firstName}`, + name: `${publicationVersion.user.lastName} ${publicationVersion.user.firstName}`, contributorType: 'ContactPerson', nameType: 'Personal', - givenName: currentVersion.user.firstName, - familyName: currentVersion.user.lastName, + givenName: publicationVersion.user.firstName, + familyName: publicationVersion.user.lastName, nameIdentifiers: [ { - nameIdentifier: currentVersion.user.orcid, + nameIdentifier: publicationVersion.user.orcid, nameIdentifierScheme: 'ORCID', schemeUri: 'https://orcid.org/' } @@ -210,11 +209,11 @@ export const updateDOI = async ( language: 'en', types: { resourceTypeGeneral: 'Other', - resourceType: publication.type + resourceType: publicationVersion.publication.type }, relatedIdentifiers: allReferencesWithDOI, relatedItems: otherReferences, - fundingReferences: currentVersion.funders.map((funder) => ({ + fundingReferences: publicationVersion.funders.map((funder) => ({ funderName: funder.name, funderReference: funder.ror || funder.link, funderIdentifierType: funder.ror ? 'ROR' : 'Other' @@ -489,13 +488,10 @@ export const formatAffiliationName = (affiliation: I.MappedOrcidAffiliation): st }; export const createPublicationHTMLTemplate = ( - publication: I.PublicationWithVersionAttached, - references: I.Reference[] + publicationVersion: I.PublicationVersion, + references: I.Reference[], + linkedTo: I.LinkedToPublication[] ): string => { - const { type, doi, linkedTo } = publication; - - const currentVersion = publication.versions[0]; - const { title, content, @@ -510,7 +506,7 @@ export const createPublicationHTMLTemplate = ( dataPermissionsStatementProvidedBy, dataAccessStatement, selfDeclaration - } = currentVersion; + } = publicationVersion; // cheerio uses htmlparser2 // parsing the publication content can sometimes help with unpaired opening/closing tags @@ -519,16 +515,16 @@ export const createPublicationHTMLTemplate = ( const authors = coAuthors.filter((author) => Boolean(author.confirmedCoAuthor && author.linkedUser)); // If corresponding author is not found in coauthors list, and we have the necessary fields, mock them up - if (!authors.find((author) => author.linkedUser === currentVersion.createdBy)) { + if (!authors.find((author) => author.linkedUser === publicationVersion.createdBy)) { authors.unshift({ - id: currentVersion.createdBy, + id: publicationVersion.createdBy, approvalRequested: false, confirmedCoAuthor: true, createdAt: new Date(), - email: currentVersion.user.email || '', - linkedUser: currentVersion.createdBy, - publicationVersionId: currentVersion.id, - user: currentVersion.user, + email: publicationVersion.user.email || '', + linkedUser: publicationVersion.createdBy, + publicationVersionId: publicationVersion.id, + user: publicationVersion.user, reminderDate: null, isIndependent: true, affiliations: [] @@ -758,12 +754,12 @@ export const createPublicationHTMLTemplate = ( .join(', ')}

@@ -775,8 +771,8 @@ export const createPublicationHTMLTemplate = (

@@ -827,7 +823,7 @@ export const createPublicationHTMLTemplate = ( ${linkedTo .map( (link) => - `

${link.publicationToRef.versions[0].title}

` + `

${link.title}

` ) .join('')} ` @@ -835,11 +831,11 @@ export const createPublicationHTMLTemplate = ( } ${ - selfDeclaration && ['PROTOCOL', 'HYPOTHESIS'].includes(type) + selfDeclaration && ['PROTOCOL', 'HYPOTHESIS'].includes(publicationVersion.publication.type) ? `
Data access statement
${ - type === 'PROTOCOL' + publicationVersion.publication.type === 'PROTOCOL' ? '

Data has not yet been collected according to this method/protocol.

' : '

Data has not yet been collected to test this hypothesis (i.e. this is a preregistration)

' } @@ -910,20 +906,19 @@ export const createPublicationHTMLTemplate = ( return htmlTemplate; }; -export const createPublicationHeaderTemplate = (publication: I.PublicationWithVersionAttached): string => { - const currentVersion = publication.versions[0]; - const authors = currentVersion.coAuthors.filter((author) => author.confirmedCoAuthor && author.linkedUser); +export const createPublicationHeaderTemplate = (publicationVersion: I.PublicationVersion): string => { + const authors = publicationVersion.coAuthors.filter((author) => author.confirmedCoAuthor && author.linkedUser); - if (!authors.find((author) => author.linkedUser === currentVersion.createdBy)) { + if (!authors.find((author) => author.linkedUser === publicationVersion.createdBy)) { authors.unshift({ - id: currentVersion.createdBy, + id: publicationVersion.createdBy, approvalRequested: false, confirmedCoAuthor: true, createdAt: new Date(), - email: currentVersion.user.email || '', - linkedUser: currentVersion.createdBy, - publicationVersionId: currentVersion.id, - user: currentVersion.user, + email: publicationVersion.user.email || '', + linkedUser: publicationVersion.createdBy, + publicationVersionId: publicationVersion.id, + user: publicationVersion.user, reminderDate: null, isIndependent: true, affiliations: [] @@ -966,13 +961,15 @@ export const createPublicationHeaderTemplate = (publication: I.PublicationWithVe Published ${ - currentVersion.publishedDate ? formatPDFDate(currentVersion.publishedDate) : formatPDFDate(new Date()) + publicationVersion.publishedDate + ? formatPDFDate(publicationVersion.publishedDate) + : formatPDFDate(new Date()) }
`; }; -export const createPublicationFooterTemplate = (publication: I.PublicationWithVersionAttached): string => { +export const createPublicationFooterTemplate = (publicationVersion: I.PublicationVersion): string => { const base64InterRegular = fs.readFileSync('assets/fonts/Inter-Regular.ttf', { encoding: 'base64' }); const base64OctopusLogo = fs.readFileSync('assets/img/OCTOPUS_LOGO_ILLUSTRATION_WHITE_500PX.svg', { encoding: 'base64' @@ -1026,7 +1023,7 @@ export const createPublicationFooterTemplate = (publication: I.PublicationWithVe