Skip to content

Commit

Permalink
Merge pull request #639 from desci-labs/doi-oa-bookmarks
Browse files Browse the repository at this point in the history
Bookmarks Update
  • Loading branch information
hubsmoke authored Nov 19, 2024
2 parents fa2c30d + 86f4274 commit ba85873
Show file tree
Hide file tree
Showing 12 changed files with 531 additions and 252 deletions.
26 changes: 26 additions & 0 deletions desci-server/prisma/migrations/20241118075235_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
Warnings:
- A unique constraint covering the columns `[userId,type,nodeUuid,doi,oaWorkId]` on the table `BookmarkedNode` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateEnum
CREATE TYPE "BookmarkType" AS ENUM ('NODE', 'DOI', 'OA');

-- DropForeignKey
ALTER TABLE "BookmarkedNode" DROP CONSTRAINT "BookmarkedNode_nodeUuid_fkey";

-- DropIndex
DROP INDEX "BookmarkedNode_userId_nodeUuid_key";

-- AlterTable
ALTER TABLE "BookmarkedNode" ADD COLUMN "doi" TEXT,
ADD COLUMN "oaWorkId" TEXT,
ADD COLUMN "type" "BookmarkType" NOT NULL DEFAULT 'NODE',
ALTER COLUMN "nodeUuid" DROP NOT NULL;

-- CreateIndex
CREATE UNIQUE INDEX "BookmarkedNode_userId_type_nodeUuid_doi_oaWorkId_key" ON "BookmarkedNode"("userId", "type", "nodeUuid", "doi", "oaWorkId");

-- AddForeignKey
ALTER TABLE "BookmarkedNode" ADD CONSTRAINT "BookmarkedNode_nodeUuid_fkey" FOREIGN KEY ("nodeUuid") REFERENCES "Node"("uuid") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "BookmarkedNode" ADD COLUMN "title" TEXT;
16 changes: 13 additions & 3 deletions desci-server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -472,15 +472,19 @@ model PrivateShare {
model BookmarkedNode {
id Int @id @default(autoincrement())
userId Int
nodeUuid String
nodeUuid String?
title String?
doi String?
oaWorkId String?
type BookmarkType @default(NODE) // Default for existing records
shareId String?
privateShare PrivateShare? @relation(fields: [shareId], references: [shareId])
node Node @relation(fields: [nodeUuid], references: [uuid])
node Node? @relation(fields: [nodeUuid], references: [uuid])
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, nodeUuid])
@@unique([userId, type, nodeUuid, doi, oaWorkId])
}

model NodeCover {
Expand Down Expand Up @@ -1061,3 +1065,9 @@ enum NotificationType {
DOI_ISSUANCE_STATUS
ATTESTATION_VALIDATION
}

enum BookmarkType {
NODE
DOI
OA
}
68 changes: 11 additions & 57 deletions desci-server/src/controllers/doi/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { NextFunction, Request, Response } from 'express';
import _ from 'lodash';

import { ApiError, BadRequestError, ForbiddenError, InternalError } from '../../core/ApiError.js';
import { SuccessResponse } from '../../core/ApiResponse.js';
import { InternalErrorResponse, SuccessResponse } from '../../core/ApiResponse.js';
import { DoiError, ForbiddenMintError } from '../../core/doi/error.js';
import { logger as parentLogger } from '../../logger.js';
import { RequestWithNode } from '../../middleware/authorisation.js';
import { OpenAlexWork, transformInvertedAbstractToText } from '../../services/AutomatedMetadata.js';
import { doiService } from '../../services/index.js';
import { OpenAlexService } from '../../services/OpenAlexService.js';
const pg = await import('pg').then((value) => value.default);
const { Client } = pg;

Expand Down Expand Up @@ -53,60 +54,13 @@ export const retrieveDoi = async (req: Request, res: Response, _next: NextFuncti

if (!doiQuery) throw new BadRequestError();

const doiLink = (doiQuery as string)?.startsWith('doi.org/') ? `https://${doiQuery}` : `https://doi.org/${doiQuery}`;

const client = new Client({
connectionString: process.env.OPEN_ALEX_DATABASE_URL,
connectionTimeoutMillis: 1500,
options: '-c search_path=openalex',
});

await client.connect();
logger.info({ doiQuery }, 'Retrieve DOI');

// pull record from openalex database
const { rows } = await client.query(
`select
COALESCE(wol.pdf_url, '') as pdf_url,
COALESCE(wol.landing_page_url, '') as landing_page_url,
works.title as title,
works.id as works_id,
works."type" as work_type,
works.publication_year,
works.cited_by_count as citation_count,
COALESCE(woa.oa_status, 'unknown') as oa_status,
COALESCE(source.publisher, 'unknown') as publisher,
COALESCE(source.display_name, 'unknown') as source_name,
ARRAY(
SELECT author.display_name as author_name
FROM openalex.works_authorships wauth
LEFT JOIN openalex.authors author on author.id = wauth.author_id
WHERE wauth.work_id = works.id
) as authors
from openalex.works works
left join openalex.works_best_oa_locations wol on works.id = wol.work_id
left join openalex.works_authorships wa on works.id = wa.work_id
left join openalex.works_open_access woa on woa.work_id = works.id
left join openalex.sources source on source.id = wol.source_id
where works.doi = $1
group by wol.pdf_url, wol.landing_page_url, works.title, works.id, works."type", works.cited_by_count, works.publication_year, woa.oa_status, source.publisher, source.display_name;`,
[doiLink],
);

const works = rows?.[0] as WorksDetails;

logger.info({ works_found: rows.length > 0, doi: doiLink }, 'Retrieve DOI Works');
const { rows: abstract_result } = await client.query(
'select works.abstract_inverted_index AS abstract FROM openalex.works works WHERE works.id = $1',
[works?.works_id],
);

const abstract_inverted_index = abstract_result[0]?.abstract as OpenAlexWork['abstract_inverted_index'];
const abstract = abstract_inverted_index ? transformInvertedAbstractToText(abstract_inverted_index) : '';

await client.end();

logger.info({ works }, 'OPEN ALEX QUERY');

new SuccessResponse({ abstract, doi: identifier, ...works }).send(res);
try {
const workMetadata = await OpenAlexService.getMetadataByDoi(doiQuery as string);
logger.info({ workMetadata, doiQuery }, 'OPEN ALEX QUERY success via DOI');
new SuccessResponse(workMetadata).send(res);
} catch (e) {
logger.warn({ doiQuery, error: e }, 'Error fetching DOI metadata from openAlex');
new InternalErrorResponse('Error fetching DOI metadata from openAlex').send(res);
}
new InternalErrorResponse('Error fetching DOI metadata from openAlex').send(res);
};
68 changes: 40 additions & 28 deletions desci-server/src/controllers/nodes/bookmarks/create.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,71 @@
import { User } from '@prisma/client';
import { User, BookmarkType } from '@prisma/client';
import { Request, Response } from 'express';
import { z } from 'zod';

import { prisma } from '../../../client.js';
import { logger as parentLogger } from '../../../logger.js';
import { ensureUuidEndsWithDot } from '../../../utils.js';
import { BookmarkService } from '../../../services/BookmarkService.js';
export const CreateBookmarkSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal(BookmarkType.NODE),
nodeUuid: z.string().min(1),
shareKey: z.string().optional(),
}),
z.object({
type: z.literal(BookmarkType.DOI),
doi: z.string().min(1),
}),
z.object({
type: z.literal(BookmarkType.OA),
oaWorkId: z.string().min(1),
}),
]);

export type CreateNodeBookmarkReqBody = {
nodeUuid: string;
shareKey?: string;
};
type CreateBookmarkReqBody = z.infer<typeof CreateBookmarkSchema>;

export type CreateNodeBookmarkRequest = Request<never, never, CreateNodeBookmarkReqBody> & {
export type CreateBookmarkRequest = Request<never, never, CreateBookmarkReqBody> & {
user: User; // added by auth middleware
};

export type CreateNodeBookmarkResBody =
export type CreateBookmarkResBody =
| {
ok: true;
message: string;
}
| {
ok: false;
error: string;
details?: z.ZodIssue[] | string;
};

export const createNodeBookmark = async (req: CreateNodeBookmarkRequest, res: Response<CreateNodeBookmarkResBody>) => {
export const createNodeBookmark = async (req: CreateBookmarkRequest, res: Response<CreateBookmarkResBody>) => {
const user = req.user;

if (!user) throw Error('Middleware not properly setup for CreateNodeBookmark controller, requires req.user');

const { nodeUuid, shareKey } = req.body;
if (!nodeUuid) return res.status(400).json({ ok: false, error: 'nodeUuid is required' });

const logger = parentLogger.child({
module: 'PrivateShare::CreateNodeBookmarkController',
body: req.body,
module: 'Bookmarks::CreateNodeBookmarkController',
userId: user.id,
nodeUuid: nodeUuid,
shareId: shareKey,
body: req.body,
});

try {
logger.trace({}, 'Bookmarking node');
const createdBookmark = await prisma.bookmarkedNode.create({
data: {
userId: user.id,
nodeUuid: ensureUuidEndsWithDot(nodeUuid),
shareId: shareKey || null,
},
const bookmarkData = CreateBookmarkSchema.parse(req.body);

logger.trace({ type: bookmarkData.type }, 'Creating bookmark');

await BookmarkService.createBookmark({
userId: user.id,
...bookmarkData,
});

logger.trace({ createdBookmark }, 'Bookmark created successfully');
return res.status(200).json({ ok: true, message: 'Bookmark created successfully' });
return res.status(201).json({ ok: true, message: 'Bookmark created successfully' });
} catch (e) {
logger.error({ e, message: e?.message }, 'Failed to create bookmark');
return res.status(500).json({ ok: false, error: 'Failed to create bookmark for node' });
if (e instanceof z.ZodError) {
logger.warn({ error: e.errors }, 'Invalid request parameters');
return res.status(400).json({ ok: false, error: 'Invalid request parameters', details: e.errors });
}

logger.error({ e }, 'Error creating bookmark');
return res.status(500).json({ ok: false, error: 'Internal server error' });
}
};
69 changes: 42 additions & 27 deletions desci-server/src/controllers/nodes/bookmarks/delete.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { User } from '@prisma/client';
import { BookmarkType, User } from '@prisma/client';
import { Request, Response } from 'express';

import { prisma } from '../../../client.js';
import { logger as parentLogger } from '../../../logger.js';
import { ensureUuidEndsWithDot } from '../../../utils.js';
import { BookmarkService } from '../../../services/BookmarkService.js';

export type DeleteNodeBookmarkRequest = Request<{ nodeUuid: string }, never> & {
type DeleteBookmarkParams = {
type: BookmarkType;
bId: string; // nodeUuid | DOI | oaWorkId
};

export type DeleteNodeBookmarkRequest = Request<DeleteBookmarkParams, never> & {
user: User; // added by auth middleware
};

Expand All @@ -24,37 +28,48 @@ export const deleteNodeBookmark = async (req: DeleteNodeBookmarkRequest, res: Re

if (!user) throw Error('Middleware not properly setup for DeleteNodeBookmark controller, requires req.user');

const { nodeUuid } = req.params;
if (!nodeUuid) return res.status(400).json({ ok: false, error: 'nodeUuid is required' });
const { bId } = req.params;
let { type } = req.params;
type = type.toUpperCase() as BookmarkType;

if (!bId)
return res.status(400).json({ ok: false, error: 'bId param is required, either a nodeUuid, DOI, or oaWorkId' });

let deleteParams;
switch (type) {
case BookmarkType.NODE:
deleteParams = { type, nodeUuid: bId };
break;
case BookmarkType.DOI:
deleteParams = { type, doi: bId };
break;
case BookmarkType.OA:
deleteParams = { type, oaWorkId: bId };
break;
default:
return res.status(400).json({
ok: false,
error: 'Invalid bookmark type, must be NODE, DOI, or OA',
});
}

const logger = parentLogger.child({
module: 'PrivateShare::DeleteNodeBookmarkController',
body: req.body,
module: 'Bookmarks::DeleteNodeBookmarkController',
userId: user.id,
nodeUuid: nodeUuid,
type,
bookmarkUniqueId: bId,
});

try {
logger.trace({}, 'Bookmarking node');
const bookmark = await prisma.bookmarkedNode.findFirst({
where: { nodeUuid: ensureUuidEndsWithDot(nodeUuid), userId: user.id },
});
logger.trace({}, 'Deleting bookmark');
await BookmarkService.deleteBookmark(user.id, deleteParams);

if (!bookmark) {
logger.warn({}, 'Bookmark not found for node');
return res.status(404).json({ ok: false, error: 'Bookmark not found' });
}

const deleteResult = await prisma.bookmarkedNode.delete({
where: {
id: bookmark.id,
},
});

logger.trace({ deleteResult }, 'Bookmark deleted successfully');
return res.status(200).json({ ok: true, message: 'Bookmark deleted successfully' });
} catch (e) {
logger.error({ e, message: e?.message }, 'Failed to delete bookmark');
return res.status(500).json({ ok: false, error: 'Failed to delete bookmark for node' });
if (e instanceof Error && e.message === 'Bookmark not found') {
return res.status(404).json({ ok: false, error: 'Bookmark not found' });
}
logger.error({ e }, 'Failed to delete bookmark');
return res.status(500).json({ ok: false, error: 'Failed to delete bookmark' });
}
};
Loading

0 comments on commit ba85873

Please sign in to comment.