From 972f05200c4346e21baefabcbec3368af01398fe Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Thu, 10 Oct 2024 15:28:13 +0100 Subject: [PATCH] Push up knowledge of the author's name Refs #2014 --- src/Program.ts | 32 ++++-- src/zenodo.ts | 47 +++----- test/zenodo.test.ts | 268 ++++++++++++++++++++++---------------------- 3 files changed, 169 insertions(+), 178 deletions(-) diff --git a/src/Program.ts b/src/Program.ts index db694b15f..c34b3fd41 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -1,7 +1,6 @@ import { FetchHttpClient } from '@effect/platform' import { type Array, Effect, flow, Layer, Match, pipe, PubSub, Runtime } from 'effect' import type { ReadonlyNonEmptyArray } from 'fp-ts/lib/ReadonlyNonEmptyArray.js' -import * as TE from 'fp-ts/lib/TaskEither.js' import { DeprecatedLoggerEnv, DeprecatedSleepEnv, EventStore, ExpressConfig } from './Context.js' import { makeDeprecatedSleepEnv } from './DeprecatedServices.js' import * as Feedback from './Feedback/index.js' @@ -107,18 +106,31 @@ const publishFeedback = Layer.effect( ), ) + const name = yield* pipe( + Effect.promise( + getNameFromOrcid(feedback.authorId)({ orcidApiUrl, orcidApiToken, fetch, ...sleep, ...logger }), + ), + Effect.andThen( + flow( + Match.value, + Match.when({ _tag: 'Left' }, () => Effect.fail(new Feedback.UnableToPublishFeedback({}))), + Match.when({ _tag: 'Right' }, response => Effect.succeed(response.right)), + Match.exhaustive, + ), + ), + Effect.filterOrElse( + value => value !== undefined, + () => Effect.fail(new Feedback.UnableToPublishFeedback({})), + ), + ) + return yield* pipe( Effect.promise( - createFeedbackOnZenodo({ feedback, prereview })({ + createFeedbackOnZenodo({ + feedback: { ...feedback, author: { name, orcid: feedback.authorId } }, + prereview, + })({ fetch, - getNameFromOrcid: orcid => - pipe( - getNameFromOrcid(orcid)({ orcidApiUrl, orcidApiToken, fetch, ...sleep, ...logger }), - TE.filterOrElse( - name => typeof name === 'string', - () => 'unavailable', - ), - ), publicUrl, zenodoApiKey, zenodoUrl, diff --git a/src/zenodo.ts b/src/zenodo.ts index 16c5cc952..df0d3b52d 100644 --- a/src/zenodo.ts +++ b/src/zenodo.ts @@ -14,7 +14,6 @@ import * as RTE from 'fp-ts/lib/ReaderTaskEither.js' import * as RA from 'fp-ts/lib/ReadonlyArray.js' import * as RNEA from 'fp-ts/lib/ReadonlyNonEmptyArray.js' import type * as T from 'fp-ts/lib/Task.js' -import type * as TE from 'fp-ts/lib/TaskEither.js' import { constVoid, flow, identity, pipe } from 'fp-ts/lib/function.js' import { isString, toUpperCase } from 'fp-ts/lib/string.js' import httpErrors, { type HttpError } from 'http-errors' @@ -44,11 +43,10 @@ import { updateDeposition, uploadFile, } from 'zenodo-ts' -import type * as Feedback from './Feedback/index.js' import type { Prereview as PrereviewType } from './Prereview.js' import { getClubByName, getClubName } from './club-details.js' import { type SleepEnv, reloadCache, revalidateIfStale, timeoutRequest, useStaleCache } from './fetch.js' -import { plainText, sanitizeHtml } from './html.js' +import { type Html, plainText, sanitizeHtml } from './html.js' import type { Prereview as PreprintPrereview } from './preprint-reviews-page/index.js' import { type GetPreprintEnv, @@ -92,10 +90,6 @@ export interface IsReviewRequestedEnv { isReviewRequested: (preprint: PreprintId) => T.Task } -export interface GetNameFromOrcidEnv { - getNameFromOrcid: (orcid: Orcid) => TE.TaskEither<'unavailable', NonEmptyString> -} - const wasPrereviewRemoved = (id: number): R.Reader => R.asks(({ wasPrereviewRemoved }) => wasPrereviewRemoved(id)) @@ -107,9 +101,6 @@ const getPreprintSubjects = ( const isReviewRequested = (preprint: PreprintId): RT.ReaderTask => R.asks(({ isReviewRequested }) => isReviewRequested(preprint)) -const getNameFromOrcid = (orcid: Orcid): RTE.ReaderTaskEither => - R.asks(({ getNameFromOrcid }) => getNameFromOrcid(orcid)) - const getPrereviewsPageForSciety = flow( (page: number) => new URLSearchParams({ @@ -491,27 +482,24 @@ export const addAuthorToRecordOnZenodo = ( RTE.bimap(() => 'unavailable', constVoid), ) +interface FeedbackToPublish { + author: { name: NonEmptyString; orcid?: Orcid } + authorId: Orcid + feedback: Html + prereviewId: number +} + export const createFeedbackOnZenodo: (params: { - feedback: Feedback.FeedbackBeingPublished + feedback: FeedbackToPublish prereview: PrereviewType -}) => RTE.ReaderTaskEither< - GetNameFromOrcidEnv & PublicUrlEnv & ZenodoAuthenticatedEnv & L.LoggerEnv, - 'unavailable', - [Doi, number] -> = ({ feedback, prereview }) => +}) => RTE.ReaderTaskEither = ({ + feedback, + prereview, +}) => pipe( RTE.Do, - RTE.apS( - 'creator', - pipe( - getNameFromOrcid(feedback.authorId), - RTE.map(name => ({ name, orcid: feedback.authorId })), - ), - ), RTE.apSW('deposition', createEmptyDeposition()), - RTE.bindW('metadata', ({ creator }) => - RTE.fromReader(createDepositMetadataForFeedback({ creator, feedback, prereview })), - ), + RTE.apSW('metadata', RTE.fromReader(createDepositMetadataForFeedback({ feedback, prereview }))), RTE.chainW(({ deposition, metadata }) => updateDeposition(metadata, deposition)), RTE.chainFirstW( uploadFile({ @@ -525,7 +513,6 @@ export const createFeedbackOnZenodo: (params: { flow( error => ({ error: match(error) - .with('unavailable', () => ({})) .with(P.instanceOf(Error), error => error.message) .with({ status: P.number }, response => `${response.status} ${response.statusText}`) .with({ _tag: P.string }, D.draw) @@ -588,12 +575,10 @@ export const createRecordOnZenodo: ( ) function createDepositMetadataForFeedback({ - creator, feedback, prereview, }: { - creator: { name: NonEmptyString; orcid?: Orcid } - feedback: Feedback.FeedbackBeingPublished + feedback: FeedbackToPublish prereview: PrereviewType }) { return pipe( @@ -604,7 +589,7 @@ function createDepositMetadataForFeedback({ upload_type: 'publication', publication_type: 'other', title: plainText`Feedback on a PREreview of “${prereview.preprint.title}”`.toString(), - creators: [creator], + creators: [feedback.author], description: `

This Zenodo record is a permanently preserved version of feedback on a PREreview. You can view the complete PREreview and feedback at ${url.href}.

${feedback.feedback.toString()}`, diff --git a/test/zenodo.test.ts b/test/zenodo.test.ts index 011797c45..dbe6a5d5c 100644 --- a/test/zenodo.test.ts +++ b/test/zenodo.test.ts @@ -4082,147 +4082,157 @@ describe('addAuthorToRecordOnZenodo', () => { }) describe('createFeedbackOnZenodo', () => { - test.prop([fc.feedbackBeingPublished(), fc.prereview(), fc.nonEmptyString(), fc.string(), fc.origin(), fc.doi()])( - 'when the feedback can be created', - async (feedback, prereview, name, zenodoApiKey, publicUrl, feedbackDoi) => { - const emptyDeposition: EmptyDeposition = { - id: 1, - links: { - bucket: new URL('http://example.com/bucket'), - self: new URL('http://example.com/self'), - }, - metadata: { - prereserve_doi: { - doi: feedbackDoi, - }, - }, - state: 'unsubmitted', - submitted: false, - } - const unsubmittedDeposition: UnsubmittedDeposition = { - id: 1, - links: { - bucket: new URL('http://example.com/bucket'), - publish: new URL('http://example.com/publish'), - self: new URL('http://example.com/self'), - }, - metadata: { - creators: [{ name: 'A PREreviewer' }], - description: 'Description', - prereserve_doi: { - doi: feedbackDoi, - }, - title: 'Title', - upload_type: 'publication', - publication_type: 'other', - }, - state: 'unsubmitted', - submitted: false, - } - const submittedDeposition: SubmittedDeposition = { - id: 1, - links: { - edit: new URL('http://example.com/edit'), + test.prop([ + fc.record({ + author: fc.record({ name: fc.nonEmptyString(), orcid: fc.orcid() }, { requiredKeys: ['name'] }), + authorId: fc.orcid(), + feedback: fc.html(), + prereviewId: fc.integer(), + }), + fc.prereview(), + fc.string(), + fc.origin(), + fc.doi(), + ])('when the feedback can be created', async (feedback, prereview, zenodoApiKey, publicUrl, feedbackDoi) => { + const emptyDeposition: EmptyDeposition = { + id: 1, + links: { + bucket: new URL('http://example.com/bucket'), + self: new URL('http://example.com/self'), + }, + metadata: { + prereserve_doi: { + doi: feedbackDoi, }, - metadata: { - creators: [{ name: 'A PREreviewer' }], - description: 'Description', + }, + state: 'unsubmitted', + submitted: false, + } + const unsubmittedDeposition: UnsubmittedDeposition = { + id: 1, + links: { + bucket: new URL('http://example.com/bucket'), + publish: new URL('http://example.com/publish'), + self: new URL('http://example.com/self'), + }, + metadata: { + creators: [{ name: 'A PREreviewer' }], + description: 'Description', + prereserve_doi: { doi: feedbackDoi, - title: 'Title', - upload_type: 'publication', - publication_type: 'other', }, - state: 'done', - submitted: true, - } - const reviewUrl = `${publicUrl.href.slice(0, -1)}${format(reviewMatch.formatter, { id: prereview.id })}` - const fetch = fetchMock.sandbox() - const actual = await _.createFeedbackOnZenodo({ feedback, prereview })({ - clock: SystemClock, - fetch: fetch - .postOnce( - { - url: 'https://zenodo.org/api/deposit/depositions', - body: {}, - }, - { - body: EmptyDepositionC.encode(emptyDeposition), - status: Status.Created, - }, - ) - .putOnce( - { - url: 'http://example.com/self', - body: { - metadata: { - upload_type: 'publication', - publication_type: 'other', - title: plainText`Feedback on a PREreview of “${prereview.preprint.title}”`.toString(), - creators: [{ name, orcid: feedback.authorId }], - description: `

This Zenodo record is a permanently preserved version of feedback on a PREreview. You can view the complete PREreview and feedback at ${reviewUrl}.

+ title: 'Title', + upload_type: 'publication', + publication_type: 'other', + }, + state: 'unsubmitted', + submitted: false, + } + const submittedDeposition: SubmittedDeposition = { + id: 1, + links: { + edit: new URL('http://example.com/edit'), + }, + metadata: { + creators: [{ name: 'A PREreviewer' }], + description: 'Description', + doi: feedbackDoi, + title: 'Title', + upload_type: 'publication', + publication_type: 'other', + }, + state: 'done', + submitted: true, + } + const reviewUrl = `${publicUrl.href.slice(0, -1)}${format(reviewMatch.formatter, { id: prereview.id })}` + const fetch = fetchMock.sandbox() + const actual = await _.createFeedbackOnZenodo({ feedback, prereview })({ + clock: SystemClock, + fetch: fetch + .postOnce( + { + url: 'https://zenodo.org/api/deposit/depositions', + body: {}, + }, + { + body: EmptyDepositionC.encode(emptyDeposition), + status: Status.Created, + }, + ) + .putOnce( + { + url: 'http://example.com/self', + body: { + metadata: { + upload_type: 'publication', + publication_type: 'other', + title: plainText`Feedback on a PREreview of “${prereview.preprint.title}”`.toString(), + creators: [feedback.author], + description: `

This Zenodo record is a permanently preserved version of feedback on a PREreview. You can view the complete PREreview and feedback at ${reviewUrl}.

${feedback.feedback.toString()}`, - communities: [{ identifier: 'prereview-reviews' }], - related_identifiers: [ - { - ..._.toExternalIdentifier(prereview.preprint.id), - relation: 'references', - resource_type: 'publication-preprint', - }, - { - identifier: prereview.doi, - relation: 'references', - resource_type: 'publication-peerreview', - scheme: 'doi', - }, - ], - }, + communities: [{ identifier: 'prereview-reviews' }], + related_identifiers: [ + { + ..._.toExternalIdentifier(prereview.preprint.id), + relation: 'references', + resource_type: 'publication-preprint', + }, + { + identifier: prereview.doi, + relation: 'references', + resource_type: 'publication-peerreview', + scheme: 'doi', + }, + ], }, }, - { - body: UnsubmittedDepositionC.encode(unsubmittedDeposition), - status: Status.OK, - }, - ) - .putOnce( - { - url: 'http://example.com/bucket/feedback.html', - headers: { 'Content-Type': 'application/octet-stream' }, - functionMatcher: (_, req) => req.body === feedback.feedback.toString(), - }, - { - status: Status.Created, - }, - ) - .postOnce('http://example.com/publish', { - body: SubmittedDepositionC.encode(submittedDeposition), - status: Status.Accepted, - }), - getNameFromOrcid: () => TE.right(name), - logger: () => IO.of(undefined), - publicUrl, - zenodoApiKey, - })() + }, + { + body: UnsubmittedDepositionC.encode(unsubmittedDeposition), + status: Status.OK, + }, + ) + .putOnce( + { + url: 'http://example.com/bucket/feedback.html', + headers: { 'Content-Type': 'application/octet-stream' }, + functionMatcher: (_, req) => req.body === feedback.feedback.toString(), + }, + { + status: Status.Created, + }, + ) + .postOnce('http://example.com/publish', { + body: SubmittedDepositionC.encode(submittedDeposition), + status: Status.Accepted, + }), + logger: () => IO.of(undefined), + publicUrl, + zenodoApiKey, + })() - expect(actual).toStrictEqual(E.right([feedbackDoi, 1])) - }, - ) + expect(actual).toStrictEqual(E.right([feedbackDoi, 1])) + }) test.prop([ - fc.feedbackBeingPublished(), + fc.record({ + author: fc.record({ name: fc.nonEmptyString(), orcid: fc.orcid() }, { requiredKeys: ['name'] }), + authorId: fc.orcid(), + feedback: fc.html(), + prereviewId: fc.integer(), + }), fc.prereview(), - fc.nonEmptyString(), fc.string(), fc.origin(), fc.oneof( fc.fetchResponse({ status: fc.integer({ min: 400 }) }).map(response => Promise.resolve(response)), fc.error().map(error => Promise.reject(error)), ), - ])('Zenodo is unavailable', async (feedback, prereview, name, zenodoApiKey, publicUrl, response) => { + ])('Zenodo is unavailable', async (feedback, prereview, zenodoApiKey, publicUrl, response) => { const actual = await _.createFeedbackOnZenodo({ feedback, prereview })({ clock: SystemClock, fetch: () => response, - getNameFromOrcid: () => TE.right(name), logger: () => IO.of(undefined), publicUrl, zenodoApiKey, @@ -4230,22 +4240,6 @@ ${feedback.feedback.toString()}`, expect(actual).toStrictEqual(E.left('unavailable')) }) - - test.prop([fc.feedbackBeingPublished(), fc.prereview(), fc.string(), fc.origin()])( - 'the name is unavailable', - async (feedback, prereview, zenodoApiKey, publicUrl) => { - const actual = await _.createFeedbackOnZenodo({ feedback, prereview })({ - clock: SystemClock, - fetch: shouldNotBeCalled, - getNameFromOrcid: () => TE.left('unavailable'), - logger: () => IO.of(undefined), - publicUrl, - zenodoApiKey, - })() - - expect(actual).toStrictEqual(E.left('unavailable')) - }, - ) }) describe('createRecordOnZenodo', () => {