From 70c8b9516af6d1a42a2b53dbcf1a9833c37a6645 Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Mon, 18 Nov 2024 06:28:36 +0000 Subject: [PATCH 01/13] improve create bookmark controller --- .../src/controllers/nodes/bookmarks/create.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/desci-server/src/controllers/nodes/bookmarks/create.ts b/desci-server/src/controllers/nodes/bookmarks/create.ts index 54ff5f5c9..1fed98724 100644 --- a/desci-server/src/controllers/nodes/bookmarks/create.ts +++ b/desci-server/src/controllers/nodes/bookmarks/create.ts @@ -1,10 +1,16 @@ import { User } 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'; +const CreateBookmarkSchema = z.object({ + nodeUuid: z.string().min(1), + shareKey: z.string().optional(), +}); + export type CreateNodeBookmarkReqBody = { nodeUuid: string; shareKey?: string; @@ -22,6 +28,7 @@ export type CreateNodeBookmarkResBody = | { ok: false; error: string; + details?: z.ZodIssue[] | string; }; export const createNodeBookmark = async (req: CreateNodeBookmarkRequest, res: Response) => { @@ -29,11 +36,10 @@ export const createNodeBookmark = async (req: CreateNodeBookmarkRequest, res: Re 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 { nodeUuid, shareKey } = CreateBookmarkSchema.parse(req.body); const logger = parentLogger.child({ - module: 'PrivateShare::CreateNodeBookmarkController', + module: 'Bookmarks::CreateNodeBookmarkController', body: req.body, userId: user.id, nodeUuid: nodeUuid, @@ -51,9 +57,14 @@ export const createNodeBookmark = async (req: CreateNodeBookmarkRequest, res: Re }); 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' }); } }; From 3dc004511758cbd20104fc6778e8d9092eaca7ce Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Mon, 18 Nov 2024 07:54:11 +0000 Subject: [PATCH 02/13] expand bookmarks migration, add doi/oa --- .../migrations/20241118075235_/migration.sql | 26 +++++++++++++++++++ desci-server/prisma/schema.prisma | 15 ++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 desci-server/prisma/migrations/20241118075235_/migration.sql diff --git a/desci-server/prisma/migrations/20241118075235_/migration.sql b/desci-server/prisma/migrations/20241118075235_/migration.sql new file mode 100644 index 000000000..5a6d51502 --- /dev/null +++ b/desci-server/prisma/migrations/20241118075235_/migration.sql @@ -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; diff --git a/desci-server/prisma/schema.prisma b/desci-server/prisma/schema.prisma index 2df5fae83..bad8b63fe 100755 --- a/desci-server/prisma/schema.prisma +++ b/desci-server/prisma/schema.prisma @@ -472,15 +472,18 @@ model PrivateShare { model BookmarkedNode { id Int @id @default(autoincrement()) userId Int - nodeUuid String + nodeUuid 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 { @@ -1061,3 +1064,9 @@ enum NotificationType { DOI_ISSUANCE_STATUS ATTESTATION_VALIDATION } + +enum BookmarkType { + NODE + DOI + OA +} From 7f42323313085df2d806c0df0265e2321d349a13 Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Mon, 18 Nov 2024 08:11:29 +0000 Subject: [PATCH 03/13] updated create bookmark controller for new bookmark types --- .../src/controllers/nodes/bookmarks/create.ts | 72 ++++++++++++------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/desci-server/src/controllers/nodes/bookmarks/create.ts b/desci-server/src/controllers/nodes/bookmarks/create.ts index 1fed98724..dd9657588 100644 --- a/desci-server/src/controllers/nodes/bookmarks/create.ts +++ b/desci-server/src/controllers/nodes/bookmarks/create.ts @@ -1,4 +1,4 @@ -import { User } from '@prisma/client'; +import { User, BookmarkType } from '@prisma/client'; import { Request, Response } from 'express'; import { z } from 'zod'; @@ -6,21 +6,29 @@ import { prisma } from '../../../client.js'; import { logger as parentLogger } from '../../../logger.js'; import { ensureUuidEndsWithDot } from '../../../utils.js'; -const CreateBookmarkSchema = z.object({ - nodeUuid: z.string().min(1), - shareKey: z.string().optional(), -}); +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; -export type CreateNodeBookmarkRequest = Request & { +export type CreateBookmarkRequest = Request & { user: User; // added by auth middleware }; -export type CreateNodeBookmarkResBody = +export type CreateBookmarkResBody = | { ok: true; message: string; @@ -31,30 +39,46 @@ export type CreateNodeBookmarkResBody = details?: z.ZodIssue[] | string; }; -export const createNodeBookmark = async (req: CreateNodeBookmarkRequest, res: Response) => { +export const createNodeBookmark = async (req: CreateBookmarkRequest, res: Response) => { const user = req.user; if (!user) throw Error('Middleware not properly setup for CreateNodeBookmark controller, requires req.user'); - const { nodeUuid, shareKey } = CreateBookmarkSchema.parse(req.body); + const bookmarkData = CreateBookmarkSchema.parse(req.body); + // const { nodeUuid, shareKey, doi, oaWorkId } = CreateBookmarkSchema.parse(req.body); const logger = parentLogger.child({ module: 'Bookmarks::CreateNodeBookmarkController', - body: req.body, 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, - }, - }); + logger.trace({ type: bookmarkData.type }, 'Creating bookmark'); + + const data = { + userId: user.id, + type: bookmarkData.type, + nodeUuid: null, + doi: null, + oaWorkId: null, + shareId: null, + }; + + switch (bookmarkData.type) { + case BookmarkType.NODE: + data.nodeUuid = ensureUuidEndsWithDot(bookmarkData.nodeUuid); + data.shareId = bookmarkData.shareKey || null; + break; + case BookmarkType.DOI: + data.doi = bookmarkData.doi; + break; + case BookmarkType.OA: + data.oaWorkId = bookmarkData.oaWorkId; + break; + } + + const createdBookmark = await prisma.bookmarkedNode.create({ data }); logger.trace({ createdBookmark }, 'Bookmark created successfully'); return res.status(201).json({ ok: true, message: 'Bookmark created successfully' }); From 74a1ff47df4802882d54ed6e0f2e785547e5dd8e Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:31:12 +0000 Subject: [PATCH 04/13] separate bookmark business logic into its own service file --- .../src/controllers/nodes/bookmarks/create.ts | 37 +++----------- desci-server/src/services/BookmarkService.ts | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+), 30 deletions(-) create mode 100644 desci-server/src/services/BookmarkService.ts diff --git a/desci-server/src/controllers/nodes/bookmarks/create.ts b/desci-server/src/controllers/nodes/bookmarks/create.ts index dd9657588..162cfd0b2 100644 --- a/desci-server/src/controllers/nodes/bookmarks/create.ts +++ b/desci-server/src/controllers/nodes/bookmarks/create.ts @@ -2,11 +2,9 @@ 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'; - -const CreateBookmarkSchema = z.discriminatedUnion('type', [ +import { BookmarkService } from '../../../services/BookmarkService.js'; +export const CreateBookmarkSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal(BookmarkType.NODE), nodeUuid: z.string().min(1), @@ -44,9 +42,6 @@ export const createNodeBookmark = async (req: CreateBookmarkRequest, res: Respon if (!user) throw Error('Middleware not properly setup for CreateNodeBookmark controller, requires req.user'); - const bookmarkData = CreateBookmarkSchema.parse(req.body); - // const { nodeUuid, shareKey, doi, oaWorkId } = CreateBookmarkSchema.parse(req.body); - const logger = parentLogger.child({ module: 'Bookmarks::CreateNodeBookmarkController', userId: user.id, @@ -54,33 +49,15 @@ export const createNodeBookmark = async (req: CreateBookmarkRequest, res: Respon }); try { + const bookmarkData = CreateBookmarkSchema.parse(req.body); + logger.trace({ type: bookmarkData.type }, 'Creating bookmark'); - const data = { + await BookmarkService.createBookmark({ userId: user.id, - type: bookmarkData.type, - nodeUuid: null, - doi: null, - oaWorkId: null, - shareId: null, - }; - - switch (bookmarkData.type) { - case BookmarkType.NODE: - data.nodeUuid = ensureUuidEndsWithDot(bookmarkData.nodeUuid); - data.shareId = bookmarkData.shareKey || null; - break; - case BookmarkType.DOI: - data.doi = bookmarkData.doi; - break; - case BookmarkType.OA: - data.oaWorkId = bookmarkData.oaWorkId; - break; - } - - const createdBookmark = await prisma.bookmarkedNode.create({ data }); + ...bookmarkData, + }); - logger.trace({ createdBookmark }, 'Bookmark created successfully'); return res.status(201).json({ ok: true, message: 'Bookmark created successfully' }); } catch (e) { if (e instanceof z.ZodError) { diff --git a/desci-server/src/services/BookmarkService.ts b/desci-server/src/services/BookmarkService.ts new file mode 100644 index 000000000..9b7beb960 --- /dev/null +++ b/desci-server/src/services/BookmarkService.ts @@ -0,0 +1,49 @@ +import { BookmarkType, BookmarkedNode, Prisma } from '@prisma/client'; +import { z } from 'zod'; + +import { prisma } from '../client.js'; +import { CreateBookmarkSchema } from '../controllers/nodes/bookmarks/create.js'; +import { logger as parentLogger } from '../logger.js'; +import { ensureUuidEndsWithDot } from '../utils.js'; + +const logger = parentLogger.child({ + module: 'Bookmarks::BookmarkService', +}); + +type CreateBookmarkData = { + userId: number; +} & z.infer; + +export const createBookmark = async (data: CreateBookmarkData): Promise => { + logger.info({ data }, 'Creating bookmark'); + + const prismaData = { + userId: data.userId, + type: data.type, + }; + + const extraData = (() => { + switch (data.type) { + case BookmarkType.NODE: + return { + nodeUuid: ensureUuidEndsWithDot(data.nodeUuid), + shareId: data.shareKey || null, + }; + case BookmarkType.DOI: + return { doi: data.doi }; + case BookmarkType.OA: + return { oaWorkId: data.oaWorkId }; + } + })(); + + const bookmark = await prisma.bookmarkedNode.create({ + data: { ...prismaData, ...extraData }, + }); + + logger.info({ bookmarkId: bookmark.id }, 'Bookmark created successfully'); + return bookmark; +}; + +export const BookmarkService = { + createBookmark, +}; From 45ffd3573173f8522625d9ca155dd941578e06dc Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:01:18 +0000 Subject: [PATCH 05/13] Refactor delete controller, extract core logic into service, change to be able to delete uuid/oa/doi bookmarks --- .../src/controllers/nodes/bookmarks/delete.ts | 66 +++++++++++-------- desci-server/src/routes/v1/nodes.ts | 15 ++++- desci-server/src/services/BookmarkService.ts | 42 ++++++++++++ 3 files changed, 94 insertions(+), 29 deletions(-) diff --git a/desci-server/src/controllers/nodes/bookmarks/delete.ts b/desci-server/src/controllers/nodes/bookmarks/delete.ts index af0264086..084664b35 100644 --- a/desci-server/src/controllers/nodes/bookmarks/delete.ts +++ b/desci-server/src/controllers/nodes/bookmarks/delete.ts @@ -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 & { user: User; // added by auth middleware }; @@ -24,37 +28,45 @@ 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 { type, bId } = req.params; + 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 }, - }); - - 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({}, 'Deleting bookmark'); + await BookmarkService.deleteBookmark(user.id, deleteParams); - 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' }); } }; diff --git a/desci-server/src/routes/v1/nodes.ts b/desci-server/src/routes/v1/nodes.ts index 60fbf5403..675e5ffca 100755 --- a/desci-server/src/routes/v1/nodes.ts +++ b/desci-server/src/routes/v1/nodes.ts @@ -97,19 +97,28 @@ router.get( asyncHandler(checkUserPublishConsent), ); router.post('/terms', [ensureUser], consent); + +// Share router.get('/share/verify/:shareId', checkPrivateShareId); router.get('/share', [ensureUser], listSharedNodes); router.get('/share/:uuid', [ensureUser], getPrivateShare); router.post('/share/:uuid', [ensureUser], createPrivateShare); router.post('/revokeShare/:uuid', [ensureUser], revokePrivateShare); + +// Bookmarks router.get('/bookmarks', [ensureUser], listBookmarkedNodes); -router.delete('/bookmarks/:nodeUuid', [ensureUser], deleteNodeBookmark); +router.delete('/bookmarks/:type/:bId', [ensureUser], deleteNodeBookmark); router.post('/bookmarks', [ensureUser], createNodeBookmark); + +// Cover router.get('/cover/:uuid', [], getCoverImage); router.get('/cover/:uuid/:version', [], getCoverImage); + router.get('/documents/:uuid', [ensureUser, ensureNodeAccess], getNodeDocument); router.post('/documents/:uuid/actions', [ensureUser, ensureNodeAccess], dispatchDocumentChange); router.get('/thumbnails/:uuid/:manifestCid?', [attachUser], thumbnails); + +// Contributions router.post('/contributions/node/:uuid', [attachUser], getNodeContributions); router.post('/contributions/:uuid', [ensureUser, ensureWriteNodeAccess], addContributor); router.patch('/contributions/verify', [ensureUser], verifyContribution); @@ -117,8 +126,11 @@ router.patch('/contributions/:uuid', [ensureUser, ensureWriteNodeAccess], update router.delete('/contributions/:uuid', [ensureUser, ensureWriteNodeAccess], deleteContributor); router.get('/contributions/user/:userId', [], getUserContributions); router.get('/contributions/user', [ensureUser], getUserContributionsAuthed); + +// Prepub (distribution) router.post('/distribution', preparePublishPackage); router.post('/distribution/preview', [ensureUser], frontmatterPreview); +router.post('/distribution/email', [ensureUser], emailPublishPackage); // Doi api routes router.get('/:identifier/doi', [], asyncHandler(retrieveNodeDoi)); @@ -128,7 +140,6 @@ router.post( automateMetadata, ); router.post('/generate-metadata', [ensureUser, validate(generateMetadataSchema)], generateMetadata); -router.post('/distribution/email', [ensureUser], emailPublishPackage); // doi automation router.post( diff --git a/desci-server/src/services/BookmarkService.ts b/desci-server/src/services/BookmarkService.ts index 9b7beb960..70cf6aae4 100644 --- a/desci-server/src/services/BookmarkService.ts +++ b/desci-server/src/services/BookmarkService.ts @@ -44,6 +44,48 @@ export const createBookmark = async (data: CreateBookmarkData): Promise => { + logger.info({ userId, ...params }, 'Deleting bookmark'); + + const bookmark = await prisma.bookmarkedNode.findFirst({ + where: { + userId, + type: params.type, + ...(() => { + switch (params.type) { + case 'NODE': + return { nodeUuid: ensureUuidEndsWithDot(params.nodeUuid) }; + case 'DOI': + return { doi: params.doi }; + case 'OA': + return { oaWorkId: params.oaWorkId }; + } + })(), + }, + select: { + id: true, + }, + }); + + if (!bookmark) { + logger.warn({}, 'Bookmark not found'); + throw new Error('Bookmark not found'); + } + + const deletedBookmark = await prisma.bookmarkedNode.delete({ + where: { id: bookmark.id }, + }); + + logger.info({ bookmarkId: deletedBookmark.id }, 'Bookmark deleted successfully'); + return deletedBookmark; +}; + export const BookmarkService = { createBookmark, + deleteBookmark, }; From 4168a6d42b25514d1f06ed2518a82f959198d484 Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:52:10 +0000 Subject: [PATCH 06/13] titles cached in bookmarks for doi/oa bookmarks --- .../migrations/20241119084420_bookmark_titles/migration.sql | 2 ++ desci-server/prisma/schema.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 desci-server/prisma/migrations/20241119084420_bookmark_titles/migration.sql diff --git a/desci-server/prisma/migrations/20241119084420_bookmark_titles/migration.sql b/desci-server/prisma/migrations/20241119084420_bookmark_titles/migration.sql new file mode 100644 index 000000000..87b269922 --- /dev/null +++ b/desci-server/prisma/migrations/20241119084420_bookmark_titles/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "BookmarkedNode" ADD COLUMN "title" TEXT; diff --git a/desci-server/prisma/schema.prisma b/desci-server/prisma/schema.prisma index bad8b63fe..b445b9903 100755 --- a/desci-server/prisma/schema.prisma +++ b/desci-server/prisma/schema.prisma @@ -473,6 +473,7 @@ model BookmarkedNode { id Int @id @default(autoincrement()) userId Int nodeUuid String? + title String? doi String? oaWorkId String? type BookmarkType @default(NODE) // Default for existing records From 3d257b4b90c6607df1ede45d8b4da1dc70d49a65 Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:52:54 +0000 Subject: [PATCH 07/13] refactor bookmark index route/controller/service, retrieve doi info, add pagination, support retrieving specific types of bookmarks only --- .../src/controllers/nodes/bookmarks/index.ts | 106 ++++++------------ desci-server/src/services/BookmarkService.ts | 103 ++++++++++++++++- desci-server/src/services/node.ts | 7 +- 3 files changed, 141 insertions(+), 75 deletions(-) diff --git a/desci-server/src/controllers/nodes/bookmarks/index.ts b/desci-server/src/controllers/nodes/bookmarks/index.ts index ec45633d8..e41787196 100644 --- a/desci-server/src/controllers/nodes/bookmarks/index.ts +++ b/desci-server/src/controllers/nodes/bookmarks/index.ts @@ -1,9 +1,16 @@ -import { User } from '@prisma/client'; +import { BookmarkType, User } 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 { getLatestManifestFromNode } from '../../../services/manifestRepo.js'; +import { BookmarkService, FilledBookmark } from '../../../services/BookmarkService.js'; +import { PaginatedResponse } from '../../notifications/index.js'; + +export const GetBookmarksQuerySchema = z.object({ + page: z.string().regex(/^\d+$/).transform(Number).optional().default('1'), + perPage: z.string().regex(/^\d+$/).transform(Number).optional().default('20'), + type: z.enum([BookmarkType.NODE, BookmarkType.DOI, BookmarkType.OA]).optional(), +}); export type BookmarkedNode = { uuid: string; @@ -12,92 +19,49 @@ export type BookmarkedNode = { dpid?: number; shareKey: string; }; -export type ListBookmarkedNodesRequest = Request & { - user: User; // added by auth middleware +export type ListBookmarksRequest = Request & { + user: User; + query: z.infer; }; - -export type ListBookmarkedNodesResBody = - | { - ok: boolean; - bookmarkedNodes: BookmarkedNode[]; - } +export type ListBookmarksResBody = + | PaginatedResponse | { error: string; + details?: z.ZodIssue[] | string; }; -export const listBookmarkedNodes = async ( - req: ListBookmarkedNodesRequest, - res: Response, -) => { +export const listBookmarkedNodes = async (req: ListBookmarksRequest, res: Response) => { const user = req.user; if (!user) throw Error('Middleware not properly setup for ListBookmarkedNodes controller, requires req.user'); const logger = parentLogger.child({ - module: 'PrivateShare::ListBookmarkedNodesController', - body: req.body, + module: 'Bookmarks::ListBookmarksController', + query: req.query, userId: user.id, }); try { logger.trace({}, 'Retrieving bookmarked nodes for user'); - const bookmarkedNodes = await prisma.bookmarkedNode.findMany({ - where: { - userId: user.id, - }, - select: { - shareId: true, - node: { - select: { - uuid: true, - dpidAlias: true, - manifestUrl: true, - manifestDocumentId: true, - // Get published versions, if any - versions: { - where: { - OR: [{ transactionId: { not: null } }, { commitId: { not: null } }], - }, - }, - }, - }, - }, - }); - - logger.trace({ bookmarkedNodesLength: bookmarkedNodes.length }, 'Bookmarked nodes retrieved successfully'); + const query = GetBookmarksQuerySchema.parse(req.query); + const bookmarks = await BookmarkService.getBookmarks(user.id, query); - if (bookmarkedNodes?.length === 0) { - return res.status(200).json({ ok: true, bookmarkedNodes: [] }); - } - - const filledBookmarkedNodes = await Promise.all( - bookmarkedNodes.map(async ({ shareId, node }) => { - const latestManifest = await getLatestManifestFromNode(node); - const manifestDpid = latestManifest.dpid ? parseInt(latestManifest.dpid.id) : undefined; - const published = node.versions.length > 0; - - return { - uuid: node.uuid, - title: latestManifest.title, - dpid: node.dpidAlias ?? manifestDpid, - published, - shareKey: shareId, - }; - }), + logger.info( + { + totalItems: bookmarks.pagination.totalItems, + page: bookmarks.pagination.currentPage, + totalPages: bookmarks.pagination.totalPages, + }, + 'Successfully fetched bookmarks', ); - logger.trace({ filledBookmarkedNodesLength: filledBookmarkedNodes.length }, 'Bookmarked nodes filled successfully'); - if (filledBookmarkedNodes) { - logger.info( - { totalBookmarkedNodesFound: filledBookmarkedNodes.length }, - 'Bookmarked nodes retrieved successfully', - ); - return res.status(200).json({ ok: true, bookmarkedNodes: filledBookmarkedNodes }); - } + return res.status(200).json(bookmarks); } catch (e) { - logger.error({ e, message: e?.message }, 'Failed to retrieve bookmarked nodes for user'); - return res.status(500).json({ error: 'Failed to retrieve bookmarked nodes' }); + if (e instanceof z.ZodError) { + logger.warn({ error: e.errors }, 'Invalid request parameters'); + return res.status(400).json({ error: 'Invalid request parameters', details: e.errors }); + } + logger.error({ e }, 'Error fetching bookmarks'); + return res.status(500).json({ error: 'Internal server error' }); } - - return res.status(500).json({ error: 'Something went wrong' }); }; diff --git a/desci-server/src/services/BookmarkService.ts b/desci-server/src/services/BookmarkService.ts index 70cf6aae4..28753ac28 100644 --- a/desci-server/src/services/BookmarkService.ts +++ b/desci-server/src/services/BookmarkService.ts @@ -1,11 +1,16 @@ -import { BookmarkType, BookmarkedNode, Prisma } from '@prisma/client'; +import { BookmarkType, BookmarkedNode, Node, Prisma } from '@prisma/client'; import { z } from 'zod'; import { prisma } from '../client.js'; import { CreateBookmarkSchema } from '../controllers/nodes/bookmarks/create.js'; +import { GetBookmarksQuerySchema } from '../controllers/nodes/bookmarks/index.js'; +import { PaginatedResponse } from '../controllers/notifications/index.js'; import { logger as parentLogger } from '../logger.js'; import { ensureUuidEndsWithDot } from '../utils.js'; +import { getLatestManifestFromNode } from './manifestRepo.js'; +import { getDpidFromNode } from './node.js'; + const logger = parentLogger.child({ module: 'Bookmarks::BookmarkService', }); @@ -85,7 +90,103 @@ export const deleteBookmark = async (userId: number, params: DeleteBookmarkParam return deletedBookmark; }; +type GetBookmarksQuery = z.infer; + +export interface FilledBookmark { + id: number; + type: BookmarkType; + nodeUuid?: string; + doi?: string; + oaWorkId?: string; + title?: string; + published?: boolean; + dpid?: number | string; + shareKey?: string; +} + +export const getBookmarks = async ( + userId: number, + query: GetBookmarksQuery, +): Promise> => { + const { page, perPage, type } = query; + const skip = (page - 1) * perPage; + + const whereClause = { + userId, + ...(type && { type }), + }; + + const [bookmarks, totalItems] = await Promise.all([ + prisma.bookmarkedNode.findMany({ + where: whereClause, + skip, + take: perPage, + orderBy: { createdAt: 'desc' }, + include: { + node: { + select: { + uuid: true, + dpidAlias: true, + manifestUrl: true, + manifestDocumentId: true, + // Get published versions, if any + versions: { + where: { + OR: [{ transactionId: { not: null } }, { commitId: { not: null } }], + }, + }, + }, + }, + }, + }), + prisma.bookmarkedNode.count({ where: whereClause }), + ]); + + const filledBookmarks = await Promise.all( + bookmarks.map(async (bookmark) => { + const details: FilledBookmark = { + id: bookmark.id, + type: bookmark.type, + }; + + switch (bookmark.type) { + case BookmarkType.NODE: + if (bookmark.node) { + const latestManifest = await getLatestManifestFromNode(bookmark.node); + const dpid = await getDpidFromNode(bookmark.node as unknown as Node, latestManifest); + details.nodeUuid = bookmark.nodeUuid; + details.title = latestManifest.title; + details.dpid = dpid; + details.published = bookmark.node.versions.length > 0; + details.shareKey = bookmark.shareId; + } + break; + case BookmarkType.DOI: + details.doi = bookmark.doi; + details.title = bookmark.title; + break; + case BookmarkType.OA: + details.oaWorkId = bookmark.oaWorkId; + details.title = bookmark.title; + break; + } + + return details; + }), + ); + + return { + data: filledBookmarks, + pagination: { + currentPage: page, + totalPages: Math.ceil(totalItems / perPage), + totalItems, + }, + }; +}; + export const BookmarkService = { createBookmark, deleteBookmark, + getBookmarks, }; diff --git a/desci-server/src/services/node.ts b/desci-server/src/services/node.ts index d9fccf622..c0ecacecb 100644 --- a/desci-server/src/services/node.ts +++ b/desci-server/src/services/node.ts @@ -1,3 +1,4 @@ +import { ResearchObjectV1 } from '@desci-labs/desci-models'; import { Node } from '@prisma/client'; import { prisma } from '../client.js'; @@ -5,13 +6,13 @@ import { ensureUuidEndsWithDot } from '../utils.js'; import { getManifestByCid } from './data/processing.js'; -export async function getDpidFromNode(node: Node): Promise { +export async function getDpidFromNode(node: Node, manifest?: ResearchObjectV1): Promise { let dpid: string | number = node.dpidAlias; if (!dpid) { const manifestCid = node.manifestUrl; try { - const manifest = await getManifestByCid(manifestCid); - dpid = manifest?.dpid?.id; + const manifestUsed = manifest ? manifest : await getManifestByCid(manifestCid); + dpid = manifestUsed?.dpid?.id; } catch (e) { // let undefined return } From 3cba804281edfff70a91e5b29fe8969ceb9d43af Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:48:31 +0000 Subject: [PATCH 08/13] refactor open alex db queries for resuability, extracted into own service --- desci-server/src/controllers/doi/check.ts | 68 ++------- .../src/controllers/nodes/openalex.ts | 56 +------ desci-server/src/services/OpenAlexService.ts | 141 ++++++++++++++++++ 3 files changed, 156 insertions(+), 109 deletions(-) create mode 100644 desci-server/src/services/OpenAlexService.ts diff --git a/desci-server/src/controllers/doi/check.ts b/desci-server/src/controllers/doi/check.ts index 66ac4682d..1be185098 100644 --- a/desci-server/src/controllers/doi/check.ts +++ b/desci-server/src/controllers/doi/check.ts @@ -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; @@ -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); }; diff --git a/desci-server/src/controllers/nodes/openalex.ts b/desci-server/src/controllers/nodes/openalex.ts index 99c0e12e3..4ba827048 100644 --- a/desci-server/src/controllers/nodes/openalex.ts +++ b/desci-server/src/controllers/nodes/openalex.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; import { SuccessResponse } from '../../core/ApiResponse.js'; import { logger as parentLogger } from '../../logger.js'; import { OpenAlexWork, transformInvertedAbstractToText } from '../../services/AutomatedMetadata.js'; +import { OpenAlexService } from '../../services/OpenAlexService.js'; import { WorksDetails } from '../doi/check.js'; const pg = await import('pg').then((value) => value.default); const { Client } = pg; @@ -37,60 +38,11 @@ export const getOpenAlexWork = async ( workId = workId.toUpperCase(); if (!workId.startsWith('https://openalex.org/')) workId = 'https://openalex.org/' + workId; - const client = new Client({ - connectionString: process.env.OPEN_ALEX_DATABASE_URL, - connectionTimeoutMillis: 1500, - options: '-c search_path=openalex', - }); - await client.connect(); + const workMetadata = await OpenAlexService.getMetadataByWorkId(workId); - // 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.doi as doi, - 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.id = $1 - group by wol.pdf_url, wol.landing_page_url, works.title, works.id, works.doi, works."type", works.cited_by_count, works.publication_year, woa.oa_status, source.publisher, source.display_name;`, - [workId], - ); - // debugger; + logger.info({ workMetadata, workId }, 'OPEN ALEX QUERY success via workId'); - const works = rows?.[0] as WorksDetails; - - logger.info({ works_found: rows.length > 0 }, 'Retrieve OA Work success'); - 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'); - - return new SuccessResponse({ abstract, ...works }).send(res); + return new SuccessResponse(workMetadata).send(res); } catch (error) { if (error instanceof z.ZodError) { logger.warn({ error: error.errors }, 'Invalid request parameters'); diff --git a/desci-server/src/services/OpenAlexService.ts b/desci-server/src/services/OpenAlexService.ts new file mode 100644 index 000000000..282fe5232 --- /dev/null +++ b/desci-server/src/services/OpenAlexService.ts @@ -0,0 +1,141 @@ +import { Client } from 'pg'; +import { z } from 'zod'; + +import { WorksDetails } from '../controllers/doi/check.js'; +import { logger as parentLogger } from '../logger.js'; + +import { OpenAlexWork, transformInvertedAbstractToText } from './AutomatedMetadata.js'; + +const logger = parentLogger.child({ + module: 'OpenAlexService::', +}); + +const client = new Client({ + connectionString: process.env.OPEN_ALEX_DATABASE_URL, + connectionTimeoutMillis: 1500, + options: '-c search_path=openalex', +}); + +function ensureFormattedWorkId(workId: string) { + workId = workId.toUpperCase(); + if (!workId.startsWith('https://openalex.org/')) workId = 'https://openalex.org/' + workId; + return workId; +} + +function ensureFormattedDoi(doi: string) { + if (doi.startsWith('doi.org/')) doi = `https://${doi}`; + if (!doi.startsWith('https://doi.org/')) doi = `https://${doi}`; + + return doi; +} + +function getRawDoi(doi: string) { + if (doi.startsWith('doi.org/')) doi = doi.replace('doi.org/', ''); + if (doi.startsWith('https://doi.org/')) doi = doi.replace('https://doi.org/', ''); + + return doi; +} + +export async function getMetadataByWorkId(workId: string): Promise { + logger.info(`Fetching OpenAlex work: ${workId}`); + await client.connect(); + + workId = ensureFormattedWorkId(workId); + + 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.doi as doi, + 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.id = $1 + group by wol.pdf_url, wol.landing_page_url, works.title, works.id, works.doi, works."type", works.cited_by_count, works.publication_year, woa.oa_status, source.publisher, source.display_name;`, + [workId], + ); + // debugger; + const work = rows?.[0] as WorksDetails; + + const { rows: abstract_result } = await client.query( + 'select works.abstract_inverted_index AS abstract FROM openalex.works works WHERE works.id = $1', + [workId], + ); + + 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(); + return { ...work, abstract }; +} + +export async function getMetadataByDoi(doi: string): Promise { + logger.info(`Fetching OpenAlex work by DOI: ${doi}`); + doi = ensureFormattedDoi(doi); + + await client.connect(); + + // 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;`, + [doi], + ); + + const work = rows?.[0] as WorksDetails; + + logger.info({ works_found: rows.length > 0, doi: doi }, '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', + [work?.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(); + return { ...work, abstract, doi: getRawDoi(doi) }; +} + +export const OpenAlexService = { + getMetadataByWorkId, + getMetadataByDoi, +}; From 67b8039cabbd38027f8dbc3f4701ff17e004260f Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:55:47 +0000 Subject: [PATCH 09/13] cache bookmark titles for doi/oa bookmarks on creation --- desci-server/src/services/BookmarkService.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/desci-server/src/services/BookmarkService.ts b/desci-server/src/services/BookmarkService.ts index 28753ac28..cc8e95064 100644 --- a/desci-server/src/services/BookmarkService.ts +++ b/desci-server/src/services/BookmarkService.ts @@ -10,6 +10,7 @@ import { ensureUuidEndsWithDot } from '../utils.js'; import { getLatestManifestFromNode } from './manifestRepo.js'; import { getDpidFromNode } from './node.js'; +import { OpenAlexService } from './OpenAlexService.js'; const logger = parentLogger.child({ module: 'Bookmarks::BookmarkService', @@ -34,10 +35,15 @@ export const createBookmark = async (data: CreateBookmarkData): Promise ({ title: metadata.title, doi: data.doi })) + .catch((e) => ({ doi: data.doi, title: data.doi })); + } case BookmarkType.OA: - return { oaWorkId: data.oaWorkId }; + return OpenAlexService.getMetadataByWorkId(data.oaWorkId) + .then((metadata) => ({ title: metadata.title, oaWorkId: data.oaWorkId })) + .catch((e) => ({ oaWorkId: data.oaWorkId, title: data.oaWorkId })); } })(); From 97383705cfb9f51b85558643f1c6e7cbbe3c68c2 Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:48:43 +0000 Subject: [PATCH 10/13] return bookmarks with dotless uuids, default max bookmarks per page if unset --- desci-server/src/controllers/nodes/bookmarks/index.ts | 9 +-------- desci-server/src/services/BookmarkService.ts | 4 ++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/desci-server/src/controllers/nodes/bookmarks/index.ts b/desci-server/src/controllers/nodes/bookmarks/index.ts index e41787196..fc2c84ab7 100644 --- a/desci-server/src/controllers/nodes/bookmarks/index.ts +++ b/desci-server/src/controllers/nodes/bookmarks/index.ts @@ -8,17 +8,10 @@ import { PaginatedResponse } from '../../notifications/index.js'; export const GetBookmarksQuerySchema = z.object({ page: z.string().regex(/^\d+$/).transform(Number).optional().default('1'), - perPage: z.string().regex(/^\d+$/).transform(Number).optional().default('20'), + perPage: z.string().regex(/^\d+$/).transform(Number).optional(), type: z.enum([BookmarkType.NODE, BookmarkType.DOI, BookmarkType.OA]).optional(), }); -export type BookmarkedNode = { - uuid: string; - title?: string; - published?: boolean; - dpid?: number; - shareKey: string; -}; export type ListBookmarksRequest = Request & { user: User; query: z.infer; diff --git a/desci-server/src/services/BookmarkService.ts b/desci-server/src/services/BookmarkService.ts index cc8e95064..3c66e4a17 100644 --- a/desci-server/src/services/BookmarkService.ts +++ b/desci-server/src/services/BookmarkService.ts @@ -126,7 +126,7 @@ export const getBookmarks = async ( prisma.bookmarkedNode.findMany({ where: whereClause, skip, - take: perPage, + ...(perPage ? { take: perPage } : {}), orderBy: { createdAt: 'desc' }, include: { node: { @@ -160,7 +160,7 @@ export const getBookmarks = async ( if (bookmark.node) { const latestManifest = await getLatestManifestFromNode(bookmark.node); const dpid = await getDpidFromNode(bookmark.node as unknown as Node, latestManifest); - details.nodeUuid = bookmark.nodeUuid; + details.nodeUuid = bookmark.nodeUuid.replace('.', ''); details.title = latestManifest.title; details.dpid = dpid; details.published = bookmark.node.versions.length > 0; From 6bb8d2ddb015844f876002d908f9f1e5907b50d8 Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:52:11 +0000 Subject: [PATCH 11/13] fix psql import --- desci-server/src/services/OpenAlexService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desci-server/src/services/OpenAlexService.ts b/desci-server/src/services/OpenAlexService.ts index 282fe5232..2ae507e82 100644 --- a/desci-server/src/services/OpenAlexService.ts +++ b/desci-server/src/services/OpenAlexService.ts @@ -1,4 +1,5 @@ -import { Client } from 'pg'; +const pg = await import('pg').then((value) => value.default); +const { Client } = pg; import { z } from 'zod'; import { WorksDetails } from '../controllers/doi/check.js'; From d10f766b958dab49e884afba8af09b33f164f3fd Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:01:59 +0000 Subject: [PATCH 12/13] conditional skip on req without pagination, default totalPages to 1 --- desci-server/src/controllers/nodes/bookmarks/index.ts | 1 - desci-server/src/services/BookmarkService.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/desci-server/src/controllers/nodes/bookmarks/index.ts b/desci-server/src/controllers/nodes/bookmarks/index.ts index fc2c84ab7..c2980a6b1 100644 --- a/desci-server/src/controllers/nodes/bookmarks/index.ts +++ b/desci-server/src/controllers/nodes/bookmarks/index.ts @@ -25,7 +25,6 @@ export type ListBookmarksResBody = export const listBookmarkedNodes = async (req: ListBookmarksRequest, res: Response) => { const user = req.user; - if (!user) throw Error('Middleware not properly setup for ListBookmarkedNodes controller, requires req.user'); const logger = parentLogger.child({ diff --git a/desci-server/src/services/BookmarkService.ts b/desci-server/src/services/BookmarkService.ts index 3c66e4a17..1d46eabd5 100644 --- a/desci-server/src/services/BookmarkService.ts +++ b/desci-server/src/services/BookmarkService.ts @@ -125,7 +125,7 @@ export const getBookmarks = async ( const [bookmarks, totalItems] = await Promise.all([ prisma.bookmarkedNode.findMany({ where: whereClause, - skip, + ...(skip > 0 ? { skip } : {}), ...(perPage ? { take: perPage } : {}), orderBy: { createdAt: 'desc' }, include: { @@ -185,7 +185,7 @@ export const getBookmarks = async ( data: filledBookmarks, pagination: { currentPage: page, - totalPages: Math.ceil(totalItems / perPage), + totalPages: Math.ceil(totalItems / perPage) || 1, totalItems, }, }; From 86f42749782bd7122faed852ae6594c3305e227b Mon Sep 17 00:00:00 2001 From: kadami <86646883+kadamidev@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:15:13 +0000 Subject: [PATCH 13/13] unbookmark format type to match enum --- desci-server/src/controllers/nodes/bookmarks/delete.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desci-server/src/controllers/nodes/bookmarks/delete.ts b/desci-server/src/controllers/nodes/bookmarks/delete.ts index 084664b35..ea446c5ff 100644 --- a/desci-server/src/controllers/nodes/bookmarks/delete.ts +++ b/desci-server/src/controllers/nodes/bookmarks/delete.ts @@ -28,7 +28,10 @@ export const deleteNodeBookmark = async (req: DeleteNodeBookmarkRequest, res: Re if (!user) throw Error('Middleware not properly setup for DeleteNodeBookmark controller, requires req.user'); - const { type, bId } = req.params; + 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' });