Skip to content

Commit

Permalink
Merge pull request #424 from desci-labs/doi-finalismo
Browse files Browse the repository at this point in the history
Doi finalismo
  • Loading branch information
shadrach-tayo authored Jul 11, 2024
2 parents ad6830a + 12aa1a0 commit 4c3efae
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 40 deletions.
1 change: 1 addition & 0 deletions desci-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"url-safe-base64": "1.2.0",
"uuid": "^8.3.2",
"ws": "^8.15.0",
"xml-js": "^1.6.11",
"yauzl": "^2.10.0",
"zod": "^3.22.4"
},
Expand Down
96 changes: 68 additions & 28 deletions desci-server/src/controllers/doi/mint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DoiStatus } from '@prisma/client';
import sgMail from '@sendgrid/mail';
import { Request, Response, NextFunction } from 'express';
import _ from 'lodash';

Expand All @@ -14,6 +15,7 @@ import {
logger as parentLogger,
prisma,
} from '../../internal.js';
import { DoiMintedEmailHtml } from '../../templates/emails/utils/emailRenderer.js';

export const mintDoi = async (req: Request, res: Response, _next: NextFunction) => {
const { uuid } = req.params;
Expand Down Expand Up @@ -52,41 +54,79 @@ export const handleCrossrefNotificationCallback = async (

if (!submission) {
logger.error({ payload: req.payload }, 'Crossref Notifiication: pending submission not found');
throw new NotFoundError('submission not found');
// throw new NotFoundError('submission not found');
new SuccessMessageResponse().send(res);
return;
}

await doiService.updateSubmission({ id: submission.id }, { notification: req.payload });
logger.info('SUBMISSION UPDATED');

new SuccessMessageResponse().send(res);

// check retrieve url to get submission result
const response = await crossRefClient.retrieveSubmission(req.payload.retrieveUrl);
try {
// check retrieve url to get submission result
const response = await crossRefClient.retrieveSubmission(req.payload.retrieveUrl);

logger.info({ response, submission }, 'CREATE DOI CALLBACK RESPONSE');
if (response.success) {
logger.info('CREATE DOI ');
const doiRecord = await prisma.doiRecord.create({
data: {
uuid: submission.uuid,
dpid: submission.dpid,
doi: submission.uniqueDoi,
},
});
await doiService.updateSubmission(
{ id: submission.id },
{
status: DoiStatus.SUCCESS,
doiRecordId: doiRecord.id,
},
);
} else {
logger.info('ERROR CREATING DOI');
await doiService.updateSubmission(
{ id: submission.id },
{ status: response.failure ? DoiStatus.FAILED : DoiStatus.PENDING },
);
}
logger.info({ response, submission }, 'CREATE DOI CALLBACK RESPONSE');
if (response.success) {
logger.info({ response, submission }, 'CREATE DOI ');

const doiRecord = await prisma.doiRecord.create({
data: {
uuid: submission.uuid,
dpid: submission.dpid,
doi: submission.uniqueDoi,
},
});
await doiService.updateSubmission(
{ id: submission.id },
{
status: DoiStatus.SUCCESS,
doiRecordId: doiRecord.id,
},
);

// TODO: email authors about the submission status
// Send Notification Email Node author about the submission status
const node = await prisma.node.findFirst({
where: { uuid: submission.uuid },
include: { owner: { select: { email: true, name: true } } },
});

if (!node.owner.email) return;
const message = {
to: node.owner.email,
from: 'no-reply@desci.com',
subject: 'DOI Registration successful 🎉',
text: `Hello ${node.owner.name}, You DOI registration for the research object ${node.title} has been completed. Here is your DOI: ${process.env.CROSSREF_DOI_URL}/${submission.uniqueDoi}`,
html: DoiMintedEmailHtml({
dpid: submission.dpid,
userName: node.owner.name.split(' ')?.[0] ?? '',
dpidPath: `${process.env.DAPP_URL}/dpid/${submission.dpid}`,
doi: `${process.env.CROSSREF_DOI_URL}/${submission.uniqueDoi}`,
nodeTitle: node.title,
}),
};

try {
logger.info({ members: message, NODE_ENV: process.env.NODE_ENV }, 'DOI MINTED EMAIL');
if (process.env.NODE_ENV === 'production') {
const response = await sgMail.send(message);
logger.info(response, '[EMAIL]:: Response');
} else {
logger.info({ nodeEnv: process.env.NODE_ENV }, message.subject);
}
} catch (err) {
logger.info({ err }, '[ERROR]:: DOI MINTED EMAIL');
}
} else {
logger.info('ERROR CREATING DOI');
await doiService.updateSubmission(
{ id: submission.id },
{ status: response.failure ? DoiStatus.FAILED : DoiStatus.PENDING },
);
}
} catch (error) {
logger.error({ error }, 'Error updating DOI submission');
}
};
73 changes: 61 additions & 12 deletions desci-server/src/services/crossRef/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import FormData from 'form-data';
import fetch from 'node-fetch';
import { default as Remixml } from 'remixml';
import { v4 } from 'uuid';
import { xml2json } from 'xml-js';

import { prisma } from '../../client.js';
import { logger as parentLogger } from '../../logger.js';
Expand Down Expand Up @@ -341,22 +342,45 @@ class CrossRefClient {
}

async retrieveSubmission(retrieveUrl: string) {
// retrieve submission log whose batchId == param.['CROSSREF-EXTERNAL-ID']
// update with notifiication payload
// query submssion payload from param.CROSSREF-RETRIEVE-URL
// only create doi if submission status is success

try {
logger.info({ retrieveUrl }, 'ATTEMPT TO RETRIEVE SUBMISSION');
const response = (await fetch(retrieveUrl).then((res) => res.json())) as NotificationResult;
logger.info(response, 'RETRIEVE SUBMISSION');
// return interprete the response from the api to determine if the
// submission status has either `success | pending | failed`
const isSuccess = response?.completed !== null && !!response.recordCreated;
return { success: isSuccess, failure: !isSuccess };
const response = await fetch(retrieveUrl);
const contentType = response.headers.get('content-type');
logger.info({ contentType }, 'RETRIEVE SUBMISSION');
// handle when response is gone

if (contentType === 'text/xml;charset=UTF-8') {
// handle xml response
const data = await response.text();
const xmlPayload = xml2json(data);
const result = JSON.parse(xmlPayload) as NotificationResultXmlJson;
logger.info({ result }, 'PAYLOAD');
const doi_batch_diagnostic = result?.elements?.[0];
const batch_data = doi_batch_diagnostic.elements?.find((el) => el.name === 'batch_data');
const success = batch_data?.elements?.find((element) => element.name === 'success_count');
const isSuccess = success?.elements?.[0]?.text === '1';
return { success: isSuccess, failure: !isSuccess };
} else {
// handle json response
const data = await response.text();
logger.info({ data }, 'RESPONSE');
let payload: NotificationResult;
try {
payload = JSON.parse(data) as NotificationResult;
logger.info({ payload }, 'PAYLOAD');
} catch (err) {
logger.info({ err }, 'Cannot parse json body');
payload = JSON.parse(data.substring(1, data.length - 1));
logger.info({ payload }, 'PAYLOAD');
}
// return interprete the response from the api to determine if the
// submission status has either `success | pending | failed`
const isSuccess = payload?.completed !== null && !!payload?.recordCreated;
return { success: isSuccess, failure: !isSuccess };
}
} catch (err) {
logger.error({ err }, 'ERROR RETRIEVING SUBMISSION');
return { success: false, failure: true };
return { success: false, failure: false };
}
}
}
Expand All @@ -377,4 +401,29 @@ type NotificationResult = {
recordUpdated: string | null;
};

interface NotificationResultXmlJson {
elements: Array<{
type: string;
name: 'doi_batch_diagnostic';
attributes: {
status: 'completed';
sp: 'a-cs1';
};
elements: Array<{
type: 'element';
name: 'batch_data';
elements: Array<{
type: 'element';
name: 'success_count';
elements: [
{
type: 'text';
text: string;
},
];
}>;
}>;
}>;
}

// TODO: run yarn generate
81 changes: 81 additions & 0 deletions desci-server/src/templates/emails/DoiMinted.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
Body,
Container,
Column,
Head,
Heading,
Html,
Preview,
Row,
Text,
Button,
Section,
render,
} from '@react-email/components';
import * as React from 'react';

import MainLayout from './MainLayout.js';

export interface DoiMintedEmailProps {
dpid: string;
dpidPath: string;
userName: string;
nodeTitle: string;
doi: string;
}

const DoiMintedEmail = ({ dpidPath, userName, nodeTitle, doi }: DoiMintedEmailProps) => (
<MainLayout footerMsg="">
<Html>
<Head />
<Preview>{nodeTitle} just received a DOI 🎉</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1} className="text-center !text-black">
Hello ${userName}, You DOI registration for the research object ${nodeTitle} has been completed. Here is
your DOI: {doi}
</Heading>
<Section className="mx-auto w-fit my-5 bg-[#dadce0] rounded-md px-14 py-3" align="center">
<Button
href={dpidPath}
className="backdrop-blur-2xl rounded-sm"
style={{
color: 'white',
padding: '10px 20px',
marginRight: '10px',
// backdropFilter: 'blur(20px)',
background: '#28aac4',
// backgroundOpacity: '0.5',
}}
>
View Node
</Button>
</Section>
</Container>
</Body>
</Html>
</MainLayout>
);

export default DoiMintedEmail;

const main = {
backgroundColor: '#ffffff',
margin: '0 auto',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
};

const container = {
margin: '0 auto',
padding: '0px 20px',
};

const h1 = {
// color: '#000000',
fontSize: '30px',
fontWeight: '700',
margin: '30px 0',
padding: '0',
lineHeight: '42px',
};
3 changes: 3 additions & 0 deletions desci-server/src/templates/emails/utils/emailRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { render } from '@react-email/components';

import AttestationClaimedEmail, { AttestationClaimedEmailProps } from '../AttestationClaimed.js';
import ContributorInvite, { ContributorInviteEmailProps } from '../ContributorInvite.js';
import DoiMintedEmail, { DoiMintedEmailProps } from '../DoiMinted.js';
import MagicCodeEmail, { MagicCodeEmailProps } from '../MagicCode.js';
import NodeUpdated, { NodeUpdatedEmailProps } from '../NodeUpdated.js';
import SubmissionPackage, { SubmissionPackageEmailProps } from '../SubmissionPackage.js';
Expand All @@ -23,3 +24,5 @@ export const AttestationClaimedEmailHtml = (props: AttestationClaimedEmailProps)
export const NodeUpdatedEmailHtml = (props: NodeUpdatedEmailProps) => render(NodeUpdated(props));

export const SubmissionPackageEmailHtml = (props: SubmissionPackageEmailProps) => render(SubmissionPackage(props));

export const DoiMintedEmailHtml = (props: DoiMintedEmailProps) => render(DoiMintedEmail(props));
12 changes: 12 additions & 0 deletions desci-server/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15148,6 +15148,11 @@ sax@>=0.6.0:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"
integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==

sax@^1.2.4:
version "1.4.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f"
integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==

scheduler@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
Expand Down Expand Up @@ -16890,6 +16895,13 @@ ws@~8.11.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==

xml-js@^1.6.11:
version "1.6.11"
resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==
dependencies:
sax "^1.2.4"

xml2js@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499"
Expand Down

0 comments on commit 4c3efae

Please sign in to comment.