Skip to content

Commit

Permalink
Merge pull request #449 from desci-labs/bookmark-nodes
Browse files Browse the repository at this point in the history
Bookmarking nodes
  • Loading branch information
hubsmoke committed Aug 15, 2024
2 parents 292fc73 + c8c3447 commit 081f0e7
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 7 deletions.
23 changes: 23 additions & 0 deletions desci-server/prisma/migrations/20240808102242_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "BookmarkedNode" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"nodeUuid" TEXT NOT NULL,
"shareId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "BookmarkedNode_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "BookmarkedNode_userId_nodeUuid_key" ON "BookmarkedNode"("userId", "nodeUuid");

-- AddForeignKey
ALTER TABLE "BookmarkedNode" ADD CONSTRAINT "BookmarkedNode_shareId_fkey" FOREIGN KEY ("shareId") REFERENCES "PrivateShare"("shareId") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "BookmarkedNode" ADD CONSTRAINT "BookmarkedNode_nodeUuid_fkey" FOREIGN KEY ("nodeUuid") REFERENCES "Node"("uuid") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "BookmarkedNode" ADD CONSTRAINT "BookmarkedNode_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
31 changes: 24 additions & 7 deletions desci-server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ model Node {
DoiRecord DoiRecord[]
dpidAlias Int?
DoiSubmissionQueue DoiSubmissionQueue[]
BookmarkedNode BookmarkedNode[]
DeferredEmails DeferredEmails[]
@@index([ownerId])
Expand Down Expand Up @@ -194,6 +195,7 @@ model User {
PublishTaskQueue PublishTaskQueue[]
NodeContribution NodeContribution[]
OrcidPutCodes OrcidPutCodes[]
BookmarkedNode BookmarkedNode[]
DeferredEmails DeferredEmails[]
@@index([orcid])
Expand Down Expand Up @@ -449,18 +451,33 @@ model PublicDataReferenceOnIpfsMirror {
}

model PrivateShare {
id Int @id @default(autoincrement())
shareId String @unique
nodeUUID String
memo String?
node Node @relation(fields: [nodeUUID], references: [uuid])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
shareId String @unique
nodeUUID String
memo String?
node Node @relation(fields: [nodeUUID], references: [uuid])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
BookmarkedNode BookmarkedNode[]
@@index([nodeUUID])
@@index([shareId])
}

model BookmarkedNode {
id Int @id @default(autoincrement())
userId Int
nodeUuid String
shareId String?
privateShare PrivateShare? @relation(fields: [shareId], references: [shareId])
node Node @relation(fields: [nodeUuid], references: [uuid])
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, nodeUuid])
}

model NodeCover {
id Int @id @default(autoincrement())
url String
Expand Down
59 changes: 59 additions & 0 deletions desci-server/src/controllers/nodes/bookmarks/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { User } from '@prisma/client';
import { Request, Response } from 'express';

import { prisma } from '../../../client.js';
import { ensureUuidEndsWithDot } from '../../../internal.js';
import { logger as parentLogger } from '../../../logger.js';

export type CreateNodeBookmarkReqBody = {
nodeUuid: string;
shareKey?: string;
};

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

export type CreateNodeBookmarkResBody =
| {
ok: true;
message: string;
}
| {
ok: false;
error: string;
};

export const createNodeBookmark = async (req: CreateNodeBookmarkRequest, res: Response<CreateNodeBookmarkResBody>) => {
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,
userId: user.id,
nodeUuid: nodeUuid,
shareId: shareKey,
});

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

logger.trace({ createdBookmark }, 'Bookmark created successfully');
return res.status(200).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' });
}
};
60 changes: 60 additions & 0 deletions desci-server/src/controllers/nodes/bookmarks/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { User } from '@prisma/client';
import { Request, Response } from 'express';

import { prisma } from '../../../client.js';
import { ensureUuidEndsWithDot } from '../../../internal.js';
import { logger as parentLogger } from '../../../logger.js';

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

export type DeleteNodeBookmarkResBody =
| {
ok: true;
message: string;
}
| {
ok: false;
error: string;
};

export const deleteNodeBookmark = async (req: DeleteNodeBookmarkRequest, res: Response<DeleteNodeBookmarkResBody>) => {
const user = req.user;

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 logger = parentLogger.child({
module: 'PrivateShare::DeleteNodeBookmarkController',
body: req.body,
userId: user.id,
nodeUuid: nodeUuid,
});

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({ 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' });
}
};
123 changes: 123 additions & 0 deletions desci-server/src/controllers/nodes/bookmarks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { IpldUrl, ResearchObjectV1Dpid } from '@desci-labs/desci-models';
import { User } from '@prisma/client';
import { Request, Response } from 'express';

import { prisma } from '../../../client.js';
import { getLatestManifestFromNode } from '../../../internal.js';
import { logger as parentLogger } from '../../../logger.js';
import { getIndexedResearchObjects } from '../../../theGraph.js';

export type BookmarkedNode = {
uuid: string;
manifestCid: string;
title?: string;
versions: number;
coverImageCid?: string | IpldUrl;
published?: boolean;
dpid?: ResearchObjectV1Dpid;
publishDate?: string;
shareKey: string;
};

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

export type ListBookmarkedNodesResBody =
| {
ok: boolean;
bookmarkedNodes: BookmarkedNode[];
}
| {
error: string;
};

export const listBookmarkedNodes = async (
req: ListBookmarkedNodesRequest,
res: Response<ListBookmarkedNodesResBody>,
) => {
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,
userId: user.id,
});

try {
logger.trace({}, 'Retrieving bookmarked nodes for user');
const bookmarkedNodes = await prisma.bookmarkedNode.findMany({
where: {
userId: user.id,
},
include: {
node: true,
},
});

logger.trace({ bookmarkedNodesLength: bookmarkedNodes.length }, 'Bookmarked nodes retrieved successfully');

if (bookmarkedNodes?.length === 0) {
return res.status(200).json({ ok: true, bookmarkedNodes: [] });
}

const nodeUuids = bookmarkedNodes.map((bm) => bm.node.uuid);
const { researchObjects } = await getIndexedResearchObjects(nodeUuids);

logger.trace({ researchObjectsLength: researchObjects.length }, 'Research objects retrieved successfully');

const publishedNodesMap = researchObjects.reduce((acc, ro) => {
try {
// convert hex string to integer
const nodeUuidInt = Buffer.from(ro.id.substring(2), 'hex');
// convert integer to hex
const nodeUuid = nodeUuidInt.toString('base64url');
acc[nodeUuid] = ro;
} catch (e) {
logger.error({ acc, ro, e, message: e?.message }, 'Failed to convert hex string to integer');
}
return acc;
}, {});

logger.trace(
{ publishedNodesMapKeyLength: Object.keys(publishedNodesMap).length },
'Published nodes map created successfully',
);

const filledBookmarkedNodes = await Promise.all(
bookmarkedNodes.map(async (bm) => {
const { node } = bm;
const latestManifest = await getLatestManifestFromNode(node);
const publishedEntry = publishedNodesMap[node.uuid];

return {
uuid: node.uuid,
manifestCid: node.manifestUrl,
title: latestManifest.title,
versions: publishedEntry?.versions.length,
coverImageCid: latestManifest.coverImage,
dpid: latestManifest.dpid,
publishDate: publishedEntry?.versions[0].time,
published: !!publishedEntry,
shareKey: bm.shareId,
};
}),
);
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 });
}
} 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' });
}

return res.status(500).json({ error: 'Something went wrong' });
};
6 changes: 6 additions & 0 deletions desci-server/src/routes/v1/nodes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Router } from 'express';

import { createNodeBookmark } from '../../controllers/nodes/bookmarks/create.js';
import { deleteNodeBookmark } from '../../controllers/nodes/bookmarks/delete.js';
import { listBookmarkedNodes } from '../../controllers/nodes/bookmarks/index.js';
import { checkIfPublishedNode } from '../../controllers/nodes/checkIfPublishedNode.js';
import { checkNodeAccess } from '../../controllers/nodes/checkNodeAccess.js';
import { addContributor } from '../../controllers/nodes/contributions/create.js';
Expand Down Expand Up @@ -89,6 +92,9 @@ router.get('/share', [ensureUser], listSharedNodes);
router.get('/share/:uuid', [ensureUser], getPrivateShare);
router.post('/share/:uuid', [ensureUser], createPrivateShare);
router.post('/revokeShare/:uuid', [ensureUser], revokePrivateShare);
router.get('/bookmarks', [ensureUser], listBookmarkedNodes);
router.delete('/bookmarks/:nodeUuid', [ensureUser], deleteNodeBookmark);
router.post('/bookmarks', [ensureUser], createNodeBookmark);
router.get('/cover/:uuid', [], getCoverImage);
router.get('/cover/:uuid/:version', [], getCoverImage);
router.get('/documents/:uuid', [ensureUser, ensureNodeAccess], getNodeDocument);
Expand Down

0 comments on commit 081f0e7

Please sign in to comment.