From 14e166ebfae9abec1e44fc740659979c07d6b4c6 Mon Sep 17 00:00:00 2001 From: shadrach-tayo Date: Thu, 26 Sep 2024 13:57:34 -0500 Subject: [PATCH 1/6] series of backend updates to support comments on node without ties to specific attestations/claim --- .../migration.sql | 14 +++++ .../migration.sql | 8 +++ desci-server/prisma/schema.prisma | 16 ++++-- .../src/controllers/attestations/comments.ts | 19 +++++-- .../attestations/recommendations.ts | 3 +- .../src/controllers/nodes/comments.ts | 35 ++++++++++++ .../src/controllers/nodes/createDpid.ts | 20 ++----- desci-server/src/controllers/nodes/index.ts | 5 ++ desci-server/src/controllers/raw/versions.ts | 8 +-- .../src/routes/v1/attestations/index.ts | 2 +- .../src/routes/v1/attestations/schema.ts | 39 +++++++++++-- desci-server/src/routes/v1/nodes.ts | 21 ++++--- desci-server/src/services/Attestation.ts | 57 ++++++++++++++----- desci-server/src/theGraph.ts | 20 +++++-- 14 files changed, 203 insertions(+), 64 deletions(-) create mode 100644 desci-server/prisma/migrations/20240925135142_add_uuid_and_visible_and_make_nodeattestation_id_optional/migration.sql create mode 100644 desci-server/prisma/migrations/20240926094958_remove_nodeid_column_from_annotation/migration.sql create mode 100644 desci-server/src/controllers/nodes/comments.ts diff --git a/desci-server/prisma/migrations/20240925135142_add_uuid_and_visible_and_make_nodeattestation_id_optional/migration.sql b/desci-server/prisma/migrations/20240925135142_add_uuid_and_visible_and_make_nodeattestation_id_optional/migration.sql new file mode 100644 index 000000000..2053e708a --- /dev/null +++ b/desci-server/prisma/migrations/20240925135142_add_uuid_and_visible_and_make_nodeattestation_id_optional/migration.sql @@ -0,0 +1,14 @@ +-- DropForeignKey +ALTER TABLE "Annotation" DROP CONSTRAINT "Annotation_nodeAttestationId_fkey"; + +-- AlterTable +ALTER TABLE "Annotation" ADD COLUMN "nodeId" INTEGER, +ADD COLUMN "uuid" TEXT, +ADD COLUMN "visible" BOOLEAN NOT NULL DEFAULT true, +ALTER COLUMN "nodeAttestationId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "Annotation" ADD CONSTRAINT "Annotation_nodeAttestationId_fkey" FOREIGN KEY ("nodeAttestationId") REFERENCES "NodeAttestation"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Annotation" ADD CONSTRAINT "Annotation_uuid_fkey" FOREIGN KEY ("uuid") REFERENCES "Node"("uuid") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/desci-server/prisma/migrations/20240926094958_remove_nodeid_column_from_annotation/migration.sql b/desci-server/prisma/migrations/20240926094958_remove_nodeid_column_from_annotation/migration.sql new file mode 100644 index 000000000..38ae5b501 --- /dev/null +++ b/desci-server/prisma/migrations/20240926094958_remove_nodeid_column_from_annotation/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `nodeId` on the `Annotation` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Annotation" DROP COLUMN "nodeId"; diff --git a/desci-server/prisma/schema.prisma b/desci-server/prisma/schema.prisma index 8d18e432f..35a756c6e 100755 --- a/desci-server/prisma/schema.prisma +++ b/desci-server/prisma/schema.prisma @@ -50,6 +50,7 @@ model Node { DoiSubmissionQueue DoiSubmissionQueue[] BookmarkedNode BookmarkedNode[] DeferredEmails DeferredEmails[] + Annotation Annotation[] @@index([ownerId]) @@index([uuid]) @@ -810,16 +811,19 @@ enum EmailType { //Comments on attestations model Annotation { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) type AnnotationType body String - highlights Json[] @default([]) - links String[] @default([]) + highlights Json[] @default([]) + links String[] @default([]) authorId Int - author User @relation(fields: [authorId], references: [id]) - nodeAttestationId Int - attestation NodeAttestation @relation(fields: [nodeAttestationId], references: [id]) + author User @relation(fields: [authorId], references: [id]) + nodeAttestationId Int? + attestation NodeAttestation? @relation(fields: [nodeAttestationId], references: [id]) deferredEmailsId Int? + uuid String? + node Node? @relation(fields: [uuid], references: [uuid]) + visible Boolean @default(true) } //An emoji reaction to a node diff --git a/desci-server/src/controllers/attestations/comments.ts b/desci-server/src/controllers/attestations/comments.ts index aebdd6e84..d50e97369 100644 --- a/desci-server/src/controllers/attestations/comments.ts +++ b/desci-server/src/controllers/attestations/comments.ts @@ -1,5 +1,5 @@ import { HighlightBlock } from '@desci-labs/desci-models'; -import { ActionType, Annotation } from '@prisma/client'; +import { ActionType, Annotation, AnnotationType } from '@prisma/client'; import { NextFunction, Request, Response } from 'express'; import _ from 'lodash'; import zod from 'zod'; @@ -13,7 +13,9 @@ import { asyncMap, attestationService, createCommentSchema, + ensureUuidEndsWithDot, logger as parentLogger, + prisma, } from '../../internal.js'; import { saveInteraction } from '../../services/interactionLog.js'; import { client } from '../../services/ipfs.js'; @@ -78,7 +80,7 @@ export const removeComment = async (req: Request, r type AddCommentBody = zod.infer; export const addComment = async (req: Request, res: Response) => { - const { authorId, claimId, body, highlights, links } = req.body; + const { authorId, claimId, body, highlights, links, uuid, visible } = req.body; const user = (req as any).user; if (parseInt(authorId.toString()) !== user.id) throw new ForbiddenError(); @@ -89,6 +91,11 @@ export const addComment = async (req: Request, user: (req as any).user, body: req.body, }); + + if (uuid) { + const node = await prisma.node.findFirst({ where: { uuid: ensureUuidEndsWithDot(uuid) } }); + if (!node) throw new NotFoundError('Node with uuid ${uuid} not found'); + } logger.trace(`addComment`); let annotation: Annotation; @@ -102,19 +109,23 @@ export const addComment = async (req: Request, }); logger.info({ processedHighlights }, 'processedHighlights'); annotation = await attestationService.createHighlight({ - claimId: parseInt(claimId.toString()), + claimId: claimId && parseInt(claimId.toString()), authorId: user.id, comment: body, links, highlights: processedHighlights as unknown as HighlightBlock[], + visible, + uuid: ensureUuidEndsWithDot(uuid), }); await saveInteraction(req, ActionType.ADD_COMMENT, { annotationId: annotation.id, claimId, authorId }); } else { annotation = await attestationService.createComment({ - claimId: parseInt(claimId.toString()), + claimId: claimId && parseInt(claimId.toString()), authorId: user.id, comment: body, links, + visible, + uuid: ensureUuidEndsWithDot(uuid), }); } await saveInteraction(req, ActionType.ADD_COMMENT, { annotationId: annotation.id, claimId, authorId }); diff --git a/desci-server/src/controllers/attestations/recommendations.ts b/desci-server/src/controllers/attestations/recommendations.ts index f20eb18b0..cea1b0d66 100644 --- a/desci-server/src/controllers/attestations/recommendations.ts +++ b/desci-server/src/controllers/attestations/recommendations.ts @@ -60,7 +60,6 @@ export const getValidatedAttestations = async (req: Request, res: Response, _nex logger.info({ communityName }); const community = await communityService.findCommunityByNameOrSlug(communityName); if (!community) throw new NotFoundError('Community not found'); - logger.info({ community }); const attestations = await attestationService.getCommunityAttestations({ communityId: community.id, @@ -83,6 +82,6 @@ export const getValidatedRecommendations = async (req: Request, res: Response, _ communityName: attestation.community.name, AttestationVersion: attestation.AttestationVersion[0], })); - logger.info({ attestations }, 'getValidatedRecommendations'); + logger.info({ recommendations: attestations.length }, 'getValidatedRecommendations'); return new SuccessResponse(response).send(res); }; diff --git a/desci-server/src/controllers/nodes/comments.ts b/desci-server/src/controllers/nodes/comments.ts new file mode 100644 index 000000000..d9149b882 --- /dev/null +++ b/desci-server/src/controllers/nodes/comments.ts @@ -0,0 +1,35 @@ +import { Response, NextFunction } from 'express'; +import _ from 'lodash'; +import z from 'zod'; + +import { + NotFoundError, + RequestWithNode, + SuccessResponse, + attestationService, + ensureUuidEndsWithDot, + getCommentsSchema, + logger, + prisma, +} from '../../internal.js'; + +export const getGeneralComments = async (req: RequestWithNode, res: Response, _next: NextFunction) => { + const { uuid } = req.params as z.infer['params']; + const node = await prisma.node.findFirst({ where: { uuid: ensureUuidEndsWithDot(uuid) } }); + if (!node) throw new NotFoundError("Can't comment on unknown research object"); + + const restrictVisibility = node.ownerId !== req?.user?.id; + + logger.info({ restrictVisibility }, 'Query Comments'); + const comments = await attestationService.getComments({ + uuid: ensureUuidEndsWithDot(uuid), + ...(restrictVisibility && { visible: true }), + }); + + const data = comments.map((comment) => { + const author = _.pick(comment.author, ['id', 'name', 'orcid']); + return { ...comment, author, highlights: comment.highlights.map((h) => JSON.parse(h as string)) }; + }); + + return new SuccessResponse(data).send(res); +}; diff --git a/desci-server/src/controllers/nodes/createDpid.ts b/desci-server/src/controllers/nodes/createDpid.ts index 2f837387e..8bb5cbf35 100644 --- a/desci-server/src/controllers/nodes/createDpid.ts +++ b/desci-server/src/controllers/nodes/createDpid.ts @@ -7,11 +7,11 @@ import { ethers } from 'ethers'; import { Response } from 'express'; import { Logger } from 'pino'; +import { CERAMIC_API_URL } from '../../config/index.js'; import { logger as parentLogger } from '../../logger.js'; import { RequestWithNode } from '../../middleware/authorisation.js'; -import { setDpidAlias } from '../../services/nodeManager.js'; -import { CERAMIC_API_URL } from '../../config/index.js'; import { getAliasRegistry, getHotWallet, getRegistryOwnerWallet } from '../../services/chain.js'; +import { setDpidAlias } from '../../services/nodeManager.js'; type DpidResponse = DpidSuccessResponse | DpidErrorResponse; export type DpidSuccessResponse = { @@ -56,9 +56,7 @@ export const createDpid = async (req: RequestWithNode, res: Response => { +export const getOrCreateDpid = async (streamId: string): Promise => { const logger = parentLogger.child({ module: 'NODE::mintDpid', ceramicStream: streamId, @@ -97,10 +95,7 @@ export const getOrCreateDpid = async ( * Note: this method in the registry contract is only callable by contract * owner, so this is not generally available. */ -export const upgradeDpid = async ( - dpid: number, - ceramicStream: string -): Promise => { +export const upgradeDpid = async (dpid: number, ceramicStream: string): Promise => { const logger = parentLogger.child({ module: 'NODE::upgradeDpid', ceramicStream, @@ -130,12 +125,7 @@ export const upgradeDpid = async ( * This should be checked before upgrading a dPID, to make sure * the new stream accurately represents the publish history. */ -const validateHistory = async ( - dpid: number, - ceramicStream: string, - registry: DpidAliasRegistry, - logger: Logger -) => { +const validateHistory = async (dpid: number, ceramicStream: string, registry: DpidAliasRegistry, logger: Logger) => { const client = newCeramicClient(CERAMIC_API_URL); const legacyEntry = await registry.legacyLookup(dpid); diff --git a/desci-server/src/controllers/nodes/index.ts b/desci-server/src/controllers/nodes/index.ts index 08a710fbf..886f695b0 100755 --- a/desci-server/src/controllers/nodes/index.ts +++ b/desci-server/src/controllers/nodes/index.ts @@ -13,3 +13,8 @@ export * from './nodesCover.js'; export * from './manager.js'; export * from './metadata.js'; export * from './doi.js'; +export * from './comments.js'; +export * from './sharedNodes.js'; +export * from './searchNodes.js'; +export * from './versionDetails.js'; +export * from './thumbnails.js'; diff --git a/desci-server/src/controllers/raw/versions.ts b/desci-server/src/controllers/raw/versions.ts index 33ca75b87..cd63bc00b 100644 --- a/desci-server/src/controllers/raw/versions.ts +++ b/desci-server/src/controllers/raw/versions.ts @@ -5,7 +5,7 @@ import { getIndexedResearchObjects, IndexedResearchObject } from '../../theGraph import { ensureUuidEndsWithDot } from '../../utils.js'; const logger = parentLogger.child({ - module: "RAW::versionsController" + module: 'RAW::versionsController', }); /** @@ -19,12 +19,10 @@ export const versions = async (req: Request, res: Response, next: NextFunction) const { researchObjects } = await getIndexedResearchObjects([uuid]); result = researchObjects[0]; } catch (err) { - logger.error( - { result, err }, `[ERROR] graph lookup fail ${err.message}`, - ); + logger.error({ result, err }, `[ERROR] graph lookup fail ${err.message}`); } if (!result) { - logger.warn({ uuid, result }, "could not find indexed versions"); + logger.warn({ uuid, result }, 'could not find indexed versions'); res.status(404).send({ ok: false, msg: `could not locate uuid ${uuid}` }); return; } diff --git a/desci-server/src/routes/v1/attestations/index.ts b/desci-server/src/routes/v1/attestations/index.ts index aa16950bd..9c17c7e04 100644 --- a/desci-server/src/routes/v1/attestations/index.ts +++ b/desci-server/src/routes/v1/attestations/index.ts @@ -62,7 +62,7 @@ router.post('/claim', [ensureUser, validate(claimAttestationSchema)], asyncHandl router.post('/unclaim', [ensureUser, validate(removeClaimSchema)], asyncHandler(removeClaim)); router.post('/claimAll', [ensureUser, validate(claimEntryAttestationsSchema)], asyncHandler(claimEntryRequirements)); -router.post('/comment', [ensureUser, validate(createCommentSchema)], asyncHandler(addComment)); +router.post('/comments', [ensureUser, validate(createCommentSchema)], asyncHandler(addComment)); router.post('/reaction', [ensureUser, validate(addReactionSchema)], asyncHandler(addReaction)); router.post('/verification', [ensureUser, validate(addVerificationSchema)], asyncHandler(addVerification)); diff --git a/desci-server/src/routes/v1/attestations/schema.ts b/desci-server/src/routes/v1/attestations/schema.ts index 7a8ec37e5..e07f904ab 100644 --- a/desci-server/src/routes/v1/attestations/schema.ts +++ b/desci-server/src/routes/v1/attestations/schema.ts @@ -34,22 +34,47 @@ export const getAttestationCommentsSchema = z.object({ }), }); +export const getCommentsSchema = z.object({ + params: z.object({ + // quickly disqualify false uuid strings + uuid: z.string().min(10), + }), +}); + +const dpidPathRegexPlusLocalResolver = + /^https?:\/\/(?dev-beta\.dpid\.org|beta\.dpid\.org|localhost:5460)\/(?\d+)\/(?v\d+)\/(?\S+.*)?/gm; + export const dpidPathRegex = - /^https:\/\/(?dev-beta|beta)\.dpid\.org\/(?\d+)\/(?v\d+)\/(?\S+.*)?/m; -// /^https:\/\/beta\.dpid\.org\/(?\d+)\/(?v\d+)\/(?\S+.*)?/m; + process.env.NODE_ENV === 'dev' + ? dpidPathRegexPlusLocalResolver + : /^https:\/\/(?dev-beta|beta)\.dpid\.org\/(?\d+)\/(?v\d+)\/(?\S+.*)?/m; + +export const uuidPathRegex = + /^https?:\/\/(?nodes-dev.desci.com|nodes.desci.com|localhost:3000)\/node\/(?[^/^.\s]+)(?\/v\d+)?(?\/root.*)?/m; export const dpidPathSchema = z .string() .url() .refine((link) => dpidPathRegex.test(link), { message: 'Invalid dpid link' }); -// TODO: UPDATE TO A UNION OF CodeHighlightBlock and PdfHighlightBlock +export const uuidPathSchema = z + .string() + .url() + .refine((link) => uuidPathRegex.test(link), { message: 'Invalid uuid link' }); + +export const resourcePathSchema = z + .string() + .url() + .refine((link) => uuidPathRegex.test(link) || dpidPathRegex.test(link), { + message: 'Invalid Resource link', + }); + const pdfHighlightSchema = z .object({ id: z.string(), text: z.string().optional(), image: z.string().optional(), - path: dpidPathSchema, + path: resourcePathSchema, startX: z.coerce.number(), startY: z.coerce.number(), endX: z.coerce.number(), @@ -81,7 +106,7 @@ const pdfHighlightSchema = z const codeHighlightSchema = z.object({ id: z.string(), text: z.string().optional(), - path: dpidPathSchema, + path: resourcePathSchema, cid: z.string(), startLine: z.coerce.number(), endLine: z.coerce.number(), @@ -93,7 +118,7 @@ const highlightBlockSchema = z.union([pdfHighlightSchema, codeHighlightSchema]); const commentSchema = z .object({ authorId: z.coerce.number(), - claimId: z.coerce.number(), + claimId: z.coerce.number().optional(), body: z.string(), links: z .string() @@ -101,6 +126,8 @@ const commentSchema = z .refine((links) => links.every((link) => dpidPathRegex.test(link))) .optional(), highlights: z.array(highlightBlockSchema).optional(), + uuid: z.string().optional(), + visible: z.boolean().default(true), }) .refine((comment) => comment.body?.length > 0 || !!comment?.highlights?.length, { message: 'Either Comment body or highlights is required', diff --git a/desci-server/src/routes/v1/nodes.ts b/desci-server/src/routes/v1/nodes.ts index 9a0b5ddab..d6281514e 100755 --- a/desci-server/src/routes/v1/nodes.ts +++ b/desci-server/src/routes/v1/nodes.ts @@ -27,7 +27,6 @@ import { draftUpdate, list, draftAddComponent, - retrieveDoi, proxyPdf, draftCreate, consent, @@ -50,15 +49,21 @@ import { automateManuscriptDoi, attachDoiSchema, retrieveNodeDoi, + prepublish, + getGeneralComments, + listSharedNodes, + searchNodes, + versionDetails, + thumbnails, } from '../../controllers/nodes/index.js'; import { retrieveTitle } from '../../controllers/nodes/legacyManifestApi.js'; import { preparePublishPackage } from '../../controllers/nodes/preparePublishPackage.js'; -import { prepublish } from '../../controllers/nodes/prepublish.js'; -import { searchNodes } from '../../controllers/nodes/searchNodes.js'; -import { listSharedNodes } from '../../controllers/nodes/sharedNodes.js'; -import { thumbnails } from '../../controllers/nodes/thumbnails.js'; -import { versionDetails } from '../../controllers/nodes/versionDetails.js'; -import { asyncHandler, attachUser, validate, ensureUserIfPresent } from '../../internal.js'; +// import { prepublish } from '../../controllers/nodes/prepublish.js'; +// import { searchNodes } from '../../controllers/nodes/searchNodes.js'; +// import { listSharedNodes } from '../../controllers/nodes/sharedNodes.js'; +// import { thumbnails } from '../../controllers/nodes/thumbnails.js'; +// import { versionDetails } from '../../controllers/nodes/versionDetails.js'; +import { asyncHandler, attachUser, validate, ensureUserIfPresent, getCommentsSchema } from '../../internal.js'; import { ensureNodeAccess, ensureWriteNodeAccess } from '../../middleware/authorisation.js'; import { ensureUser } from '../../middleware/permissions.js'; @@ -133,6 +138,8 @@ router.post( router.delete('/:uuid', [ensureUser], deleteNode); +router.get('/:uuid/comments', [validate(getCommentsSchema), attachUser], asyncHandler(getGeneralComments)); + router.get('/feed', [], feed); router.get('/legacy/retrieveTitle', retrieveTitle); diff --git a/desci-server/src/services/Attestation.ts b/desci-server/src/services/Attestation.ts index 8a7fbbb1a..c459ca777 100644 --- a/desci-server/src/services/Attestation.ts +++ b/desci-server/src/services/Attestation.ts @@ -581,21 +581,27 @@ export class AttestationService { authorId, comment, links, + uuid, + visible = true, }: { - claimId: number; + claimId?: number; authorId: number; comment: string; links: string[]; + uuid?: string; + visible: boolean; }) { assert(authorId > 0, 'Error: authorId is zero'); - assert(claimId > 0, 'Error: claimId is zero'); + claimId && assert(claimId > 0, 'Error: claimId is zero'); - const claim = await this.findClaimById(claimId); - if (!claim) throw new ClaimNotFoundError(); + if (claimId) { + const claim = await this.findClaimById(claimId); + if (!claim) throw new ClaimNotFoundError(); - const attestation = await this.findAttestationById(claim.attestationId); - if (attestation.protected) { - await this.assertUserIsMember(authorId, attestation.communityId); + const attestation = await this.findAttestationById(claim.attestationId); + if (attestation.protected) { + await this.assertUserIsMember(authorId, attestation.communityId); + } } const data: Prisma.AnnotationUncheckedCreateInput = { @@ -604,6 +610,8 @@ export class AttestationService { nodeAttestationId: claimId, body: comment, links, + uuid, + visible, }; return this.createAnnotation(data); } @@ -614,22 +622,28 @@ export class AttestationService { comment, highlights, links, + uuid, + visible, }: { claimId: number; authorId: number; comment: string; links: string[]; highlights: HighlightBlock[]; + uuid?: string; + visible: boolean; }) { assert(authorId > 0, 'Error: authorId is zero'); - assert(claimId > 0, 'Error: claimId is zero'); + claimId && assert(claimId > 0, 'Error: claimId is zero'); - const claim = await this.findClaimById(claimId); - if (!claim) throw new ClaimNotFoundError(); + if (claimId) { + const claim = await this.findClaimById(claimId); + if (!claim) throw new ClaimNotFoundError(); - const attestation = await this.findAttestationById(claim.attestationId); - if (attestation.protected) { - await this.assertUserIsMember(authorId, attestation.communityId); + const attestation = await this.findAttestationById(claim.attestationId); + if (attestation.protected) { + await this.assertUserIsMember(authorId, attestation.communityId); + } } const data: Prisma.AnnotationUncheckedCreateInput = { @@ -639,6 +653,8 @@ export class AttestationService { body: comment, links, highlights: highlights.map((h) => JSON.stringify(h)), + uuid, + visible, }; return this.createAnnotation(data); } @@ -728,6 +744,21 @@ export class AttestationService { }); } + async getComments(filter: Prisma.AnnotationWhereInput) { + logger.info({ filter }, 'GET COMMENTS'); + return prisma.annotation.findMany({ + where: filter, + include: { + author: true, + attestation: { + include: { + attestationVersion: { select: { name: true, description: true, image_url: true, createdAt: true } }, + }, + }, + }, + }); + } + /** * List all attestations and their engagements metrics across all claimed attestations * @returns AttestationWithEngagement[] diff --git a/desci-server/src/theGraph.ts b/desci-server/src/theGraph.ts index 1e3cf9475..15e602afa 100644 --- a/desci-server/src/theGraph.ts +++ b/desci-server/src/theGraph.ts @@ -77,7 +77,7 @@ export const getIndexedResearchObjects = async ( For stream resolution, build a map to allow for also returning the UUID to match the format returned by the graph lookup */ - const streamLookupMap: Record = {}; + let streamLookupMap: Record = {}; /** For legacy nodes, the graph lookup only needs the UUID */ const legacyUuids = []; @@ -100,6 +100,16 @@ export const getIndexedResearchObjects = async ( } } + /** + * fallback to _getIndexedResearchObjects() when resolving locally + * because calls to getHistoryFromStreams() never returns due to + * RESOLVER_URL not configured for local dpid resolution + */ + if (process.env.NODE_ENV === 'dev') { + legacyUuids.push(...paddedUuids); + streamLookupMap = {}; + } + let streamHistory = []; if (Object.keys(streamLookupMap).length > 0) { logger.info({ streamLookupMap }, 'Querying resolver for history'); @@ -201,7 +211,7 @@ export const _getIndexedResearchObjects = async ( export const getTimeForTxOrCommits = async (txOrCommits: string[]): Promise> => { const isTx = (id: string) => id.startsWith('0x'); const txIds = txOrCommits.filter(isTx); - const commitIdStrs = txOrCommits.filter(id => !isTx(id)); + const commitIdStrs = txOrCommits.filter((id) => !isTx(id)); const commitTimeMap = await getCommitTimestamps(commitIdStrs); const txTimeMap = await getTxTimestamps(txIds); @@ -221,15 +231,15 @@ const getTxTimestamps = async (txIds: string[]): Promise> try { const graphTxTimestamps = await getTxTimeFromGraph(txIds); const timeMap = graphTxTimestamps.reduce( - (acc, { id, time }) => ({ ...acc, [id]: time}), + (acc, { id, time }) => ({ ...acc, [id]: time }), {} as Record, ); return timeMap; } catch (err) { logger.error({ txIds, err }, 'failed to get tx timestamps from graph, returning empty map'); return {}; - }; -} + } +}; type TransactionsWithTimestamp = { researchObjectVersions: { id: string; time: string }[]; From 30fa43a86d7a749c4cce58e1c446a79dbd10bcb1 Mon Sep 17 00:00:00 2001 From: shadrach-tayo Date: Fri, 27 Sep 2024 03:17:11 -0500 Subject: [PATCH 2/6] fix type errors in tests --- desci-server/test/integration/Attestation.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/desci-server/test/integration/Attestation.test.ts b/desci-server/test/integration/Attestation.test.ts index a6a111899..91fb4310b 100644 --- a/desci-server/test/integration/Attestation.test.ts +++ b/desci-server/test/integration/Attestation.test.ts @@ -852,6 +852,7 @@ describe.only('Attestations Service', async () => { claimId: claim.id, authorId: users[1].id, comment: 'Love the attestation', + visible: true, }); }); @@ -1044,6 +1045,7 @@ describe.only('Attestations Service', async () => { claimId: openDataAttestationClaim.id, authorId: users[2].id, comment: 'I love this game', + visible: true, }); // verify one claims for node 2 attestations @@ -1054,6 +1056,7 @@ describe.only('Attestations Service', async () => { claimId: openDataAttestationClaim2.id, authorId: users[3].id, comment: 'I love this guy', + visible: true, }); }); @@ -1198,6 +1201,7 @@ describe.only('Attestations Service', async () => { claimId: openDataAttestationClaim.id, authorId: users[2].id, comment: 'I love this game', + visible: true, }); // verify one claims for node 2 attestations @@ -1208,6 +1212,7 @@ describe.only('Attestations Service', async () => { claimId: openDataAttestationClaim2.id, authorId: users[3].id, comment: 'I love this guy', + visible: true, }); await attestationService.createReaction({ claimId: claim2.id, @@ -1361,6 +1366,7 @@ describe.only('Attestations Service', async () => { claimId: openDataAttestationClaim.id, authorId: users[2].id, comment: 'I love this game', + visible: true, }); // verify one claims for node 2 attestations @@ -1372,12 +1378,14 @@ describe.only('Attestations Service', async () => { claimId: openDataAttestationClaim2.id, authorId: users[3].id, comment: 'I love this guy', + visible: true, }); await attestationService.createComment({ links: [], claimId: fairMetadataAttestationClaim2.id, authorId: users[3].id, comment: 'I love this guy', + visible: true, }); await attestationService.createReaction({ claimId: claim2.id, @@ -1397,6 +1405,7 @@ describe.only('Attestations Service', async () => { claimId: localClaim.id, authorId: users[3].id, comment: 'I love this guy', + visible: true, }); await attestationService.createReaction({ claimId: localClaim.id, @@ -1793,6 +1802,7 @@ describe.only('Attestations Service', async () => { claimId: openDataAttestationClaim.id, authorId: users[2].id, comment: 'I love this game', + visible: true, }); // verify one claims for node 2 attestations @@ -1804,12 +1814,14 @@ describe.only('Attestations Service', async () => { claimId: openDataAttestationClaim2.id, authorId: users[3].id, comment: 'I love this guy', + visible: true, }); await attestationService.createComment({ links: [], claimId: fairMetadataAttestationClaim2.id, authorId: users[3].id, comment: 'I love this guy', + visible: true, }); await attestationService.createReaction({ claimId: claim2.id, @@ -1829,6 +1841,7 @@ describe.only('Attestations Service', async () => { claimId: localClaim.id, authorId: users[3].id, comment: 'I love this guy', + visible: true, }); await attestationService.createReaction({ claimId: localClaim.id, @@ -2003,6 +2016,7 @@ describe.only('Attestations Service', async () => { claimId: openDataAttestationClaim.id, authorId: users[2].id, comment: 'I love this game', + visible: true, }); // verify one claims for node 2 attestations @@ -2013,6 +2027,7 @@ describe.only('Attestations Service', async () => { claimId: openDataAttestationClaim2.id, authorId: users[3].id, comment: 'I love this guy', + visible: true, }); }); From eee73da5d118a5f055ccbcf73dbb20f8e7f2a27b Mon Sep 17 00:00:00 2001 From: shadrach-tayo Date: Fri, 27 Sep 2024 05:31:43 -0500 Subject: [PATCH 3/6] fix: failing tests --- desci-server/test/integration/Attestation.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/desci-server/test/integration/Attestation.test.ts b/desci-server/test/integration/Attestation.test.ts index 91fb4310b..1ecd8ba62 100644 --- a/desci-server/test/integration/Attestation.test.ts +++ b/desci-server/test/integration/Attestation.test.ts @@ -133,7 +133,7 @@ const clearDatabase = async () => { await prisma.$queryRaw`TRUNCATE TABLE "Node" CASCADE;`; }; -describe.only('Attestations Service', async () => { +describe('Attestations Service', async () => { let baseManifest: ResearchObjectV1; let baseManifestCid: string; let users: User[]; @@ -2259,7 +2259,7 @@ describe.only('Attestations Service', async () => { body: 'review 1', }; let res = await request(app) - .post(`/v1/attestations/comment`) + .post(`/v1/attestations/comments`) .set('authorization', memberAuthHeaderVal1) .send(body); expect(res.statusCode).to.equal(200); @@ -2269,7 +2269,7 @@ describe.only('Attestations Service', async () => { claimId: openCodeClaim.id, body: 'review 2', }; - res = await request(app).post(`/v1/attestations/comment`).set('authorization', memberAuthHeaderVal2).send(body); + res = await request(app).post(`/v1/attestations/comments`).set('authorization', memberAuthHeaderVal2).send(body); expect(res.statusCode).to.equal(200); const comments = await attestationService.getAllClaimComments({ nodeAttestationId: openCodeClaim.id }); @@ -2280,7 +2280,7 @@ describe.only('Attestations Service', async () => { it('should prevent non community members from reviewing a protected attestation(claim)', async () => { const apiResponse = await request(app) - .post(`/v1/attestations/comment`) + .post(`/v1/attestations/comments`) .set('authorization', UserAuthHeaderVal) .send({ authorId: users[1].id, From 9eff3f540cb5954fa5e2bbbd650f14403364f160 Mon Sep 17 00:00:00 2001 From: shadrach-tayo Date: Fri, 27 Sep 2024 09:56:32 -0500 Subject: [PATCH 4/6] transform private hightlights when a node is published --- .../src/controllers/attestations/comments.ts | 4 +- .../src/controllers/nodes/prepublish.ts | 1 + desci-server/src/controllers/nodes/publish.ts | 16 ++++++ .../src/routes/v1/attestations/schema.ts | 16 ++++-- desci-server/src/services/Attestation.ts | 57 ++++++++++++++++++- 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/desci-server/src/controllers/attestations/comments.ts b/desci-server/src/controllers/attestations/comments.ts index d50e97369..343bdc472 100644 --- a/desci-server/src/controllers/attestations/comments.ts +++ b/desci-server/src/controllers/attestations/comments.ts @@ -115,7 +115,7 @@ export const addComment = async (req: Request, links, highlights: processedHighlights as unknown as HighlightBlock[], visible, - uuid: ensureUuidEndsWithDot(uuid), + ...(uuid && { uuid: ensureUuidEndsWithDot(uuid) }), }); await saveInteraction(req, ActionType.ADD_COMMENT, { annotationId: annotation.id, claimId, authorId }); } else { @@ -125,7 +125,7 @@ export const addComment = async (req: Request, comment: body, links, visible, - uuid: ensureUuidEndsWithDot(uuid), + ...(uuid && { uuid: ensureUuidEndsWithDot(uuid) }), }); } await saveInteraction(req, ActionType.ADD_COMMENT, { annotationId: annotation.id, claimId, authorId }); diff --git a/desci-server/src/controllers/nodes/prepublish.ts b/desci-server/src/controllers/nodes/prepublish.ts index 85222474f..30547f81c 100644 --- a/desci-server/src/controllers/nodes/prepublish.ts +++ b/desci-server/src/controllers/nodes/prepublish.ts @@ -82,6 +82,7 @@ export const prepublish = async (req: RequestWithNode, res: Response updateAssociatedAttestations(node.uuid, dpidAlias ? dpidAlias.toString() : manifest.dpid?.id); + const root = await prisma.publicDataReference.findFirst({ + where: { nodeId: node.id, root: true, userId: owner.id }, + orderBy: { updatedAt: 'desc' }, + }); + logger.info({ root }, 'publishDraftComments::Root'); + // publish draft comments + await attestationService.publishDraftComments({ + node, + userId: owner.id, + dpidAlias: dpidAlias ?? parseInt(manifest.dpid?.id), + rootCid: root.rootCid, + // todo: get version number + version: 0, + }); + return res.send({ ok: true, dpid: dpidAlias ?? parseInt(manifest.dpid?.id), diff --git a/desci-server/src/routes/v1/attestations/schema.ts b/desci-server/src/routes/v1/attestations/schema.ts index e07f904ab..5e1721e5e 100644 --- a/desci-server/src/routes/v1/attestations/schema.ts +++ b/desci-server/src/routes/v1/attestations/schema.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +import { logger } from '../../../logger.js'; + const communityId = z.coerce.number(); const dpid = z.coerce.number(); @@ -42,7 +44,7 @@ export const getCommentsSchema = z.object({ }); const dpidPathRegexPlusLocalResolver = - /^https?:\/\/(?dev-beta\.dpid\.org|beta\.dpid\.org|localhost:5460)\/(?\d+)\/(?v\d+)\/(?\S+.*)?/gm; + /^https?:\/\/(?dev-beta\.dpid\.org|beta\.dpid\.org|localhost:5460)\/(?\d+)\/(?v\d+)\/(?\S+.*)?/m; export const dpidPathRegex = process.env.NODE_ENV === 'dev' @@ -65,9 +67,15 @@ export const uuidPathSchema = z export const resourcePathSchema = z .string() .url() - .refine((link) => uuidPathRegex.test(link) || dpidPathRegex.test(link), { - message: 'Invalid Resource link', - }); + .refine( + (link) => { + logger.info({ uuidPathRegex: uuidPathRegex.source, dpidPathRegex: dpidPathRegex.source }, 'REGEX'); + return uuidPathRegex.test(link) || dpidPathRegex.test(link); + }, + { + message: 'Invalid Resource link', + }, + ); const pdfHighlightSchema = z .object({ diff --git a/desci-server/src/services/Attestation.ts b/desci-server/src/services/Attestation.ts index c459ca777..880de3d9b 100644 --- a/desci-server/src/services/Attestation.ts +++ b/desci-server/src/services/Attestation.ts @@ -1,7 +1,7 @@ import assert from 'assert'; import { HighlightBlock } from '@desci-labs/desci-models'; -import { AnnotationType, Attestation, Prisma, User } from '@prisma/client'; +import { AnnotationType, Attestation, Node, Prisma, User } from '@prisma/client'; import sgMail from '@sendgrid/mail'; import _ from 'lodash'; @@ -20,8 +20,10 @@ import { NotFoundError, VerificationError, VerificationNotFoundError, + asyncMap, ensureUuidEndsWithDot, logger as parentLogger, + uuidPathRegex, } from '../internal.js'; import { communityService } from '../internal.js'; import { AttestationClaimedEmailHtml } from '../templates/emails/utils/emailRenderer.js'; @@ -659,6 +661,59 @@ export class AttestationService { return this.createAnnotation(data); } + /** + * Iterate on all hidden comments and check if highlights path + * have been published then update comment to become visible + */ + async publishDraftComments({ + userId, + node, + dpidAlias, + version, + rootCid, + }: { + userId: number; + node: Node; + dpidAlias: number; + version: number; + rootCid: string; + }) { + const dpidUrl = process.env.DPID_URL_OVERRIDE ?? 'https://beta.dpid.org'; + // todo: specify version here + const dpidPrefix = `${dpidUrl}/${dpidAlias}`; + + const comments = await prisma.annotation.findMany({ where: { uuid: node.uuid, visible: false } }); + const publishedComments = await asyncMap(comments, async (comment) => { + const highlights = (comment.highlights.map((h) => JSON.parse(h as string)) ?? []) as HighlightBlock[]; + + const transformed = await asyncMap(highlights, async (highlight) => { + const match = highlight.path.match(uuidPathRegex); + logger.info({ comment: comment.id, path: highlight.path, match: match?.groups }, 'publishDraftComments::Match'); + if (!match?.groups?.path) return highlight; + + const path = match.groups.path.startsWith('/root') ? match.groups.path.substring(1) : match.groups.path; + const publicPath = path.replace('root', rootCid); + const publishedPath = await prisma.publicDataReference.findFirst({ + where: { userId, nodeId: node.id, path: publicPath }, + }); + if (!publishedPath) return highlight; + + const transformedPath = `${dpidPrefix}/${path}`; + return { ...highlight, path: transformedPath }; + }); + + logger.info({ highlights }, 'publishDraftComments::Highlights'); + logger.info({ transformed }, 'publishDraftComments::Transformed'); + return { id: comment.id, highlights: transformed.map((h) => JSON.stringify(h)) }; + }); + + logger.info({ publishedComments }, 'publishDraftComments'); + + await prisma.$transaction( + publishedComments.map((comment) => prisma.annotation.update({ where: { id: comment.id }, data: comment })), + ); + } + async removeComment(commentId: number) { return prisma.annotation.delete({ where: { id: commentId }, From 032f90ea6f51cbce7efce8fa0cf781fe681c1d8e Mon Sep 17 00:00:00 2001 From: shadrach-tayo Date: Tue, 1 Oct 2024 18:42:13 -0500 Subject: [PATCH 5/6] update visibility of private comments on publish --- desci-server/src/controllers/communities/util.ts | 6 +++--- desci-server/src/services/Attestation.ts | 10 ++++++++-- desci-server/src/theGraph.ts | 12 +----------- docker-compose.dev.yml | 6 +++--- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/desci-server/src/controllers/communities/util.ts b/desci-server/src/controllers/communities/util.ts index 71d135df6..170ad2bc7 100644 --- a/desci-server/src/controllers/communities/util.ts +++ b/desci-server/src/controllers/communities/util.ts @@ -61,13 +61,13 @@ export const resolveLatestNode = async (radar: Partial) => { }; export const getNodeVersion = async (uuid: string) => { - let indexingResults: { researchObjects: IndexedResearchObject[]}; + let indexingResults: { researchObjects: IndexedResearchObject[] }; try { indexingResults = await getIndexedResearchObjects([uuid]); const researchObject = indexingResults.researchObjects[0]; return researchObject?.versions?.length ?? 0; } catch (e) { - logger.error({ uuid, indexingResults }, "getNodeVersion failed"); + logger.error({ uuid, indexingResults }, 'getNodeVersion failed'); throw e; - }; + } }; diff --git a/desci-server/src/services/Attestation.ts b/desci-server/src/services/Attestation.ts index 880de3d9b..f7002350f 100644 --- a/desci-server/src/services/Attestation.ts +++ b/desci-server/src/services/Attestation.ts @@ -704,13 +704,19 @@ export class AttestationService { logger.info({ highlights }, 'publishDraftComments::Highlights'); logger.info({ transformed }, 'publishDraftComments::Transformed'); - return { id: comment.id, highlights: transformed.map((h) => JSON.stringify(h)) }; + return { + id: comment.id, + highlights: transformed.map((h) => JSON.stringify(h)), + visible: true, + } as Prisma.AnnotationUncheckedUpdateManyInput; }); logger.info({ publishedComments }, 'publishDraftComments'); await prisma.$transaction( - publishedComments.map((comment) => prisma.annotation.update({ where: { id: comment.id }, data: comment })), + publishedComments.map((comment) => + prisma.annotation.update({ where: { id: comment.id as number }, data: comment }), + ), ); } diff --git a/desci-server/src/theGraph.ts b/desci-server/src/theGraph.ts index 15e602afa..6fef11a88 100644 --- a/desci-server/src/theGraph.ts +++ b/desci-server/src/theGraph.ts @@ -77,7 +77,7 @@ export const getIndexedResearchObjects = async ( For stream resolution, build a map to allow for also returning the UUID to match the format returned by the graph lookup */ - let streamLookupMap: Record = {}; + const streamLookupMap: Record = {}; /** For legacy nodes, the graph lookup only needs the UUID */ const legacyUuids = []; @@ -100,16 +100,6 @@ export const getIndexedResearchObjects = async ( } } - /** - * fallback to _getIndexedResearchObjects() when resolving locally - * because calls to getHistoryFromStreams() never returns due to - * RESOLVER_URL not configured for local dpid resolution - */ - if (process.env.NODE_ENV === 'dev') { - legacyUuids.push(...paddedUuids); - streamLookupMap = {}; - } - let streamHistory = []; if (Object.keys(streamLookupMap).length > 0) { logger.info({ streamLookupMap }, 'Querying resolver for history'); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index faba1ceca..6c2504336 100755 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -231,17 +231,17 @@ services: - JAVA_OPTS=-Xmx2G -Xms2G dpid_resolver: - image: descilabs/dpid-resolver + image: descilabs/dpid-resolver:develop container_name: dpid_resolver # Uncomment and set to local repo path for tinkering # build: - # context: ~/dev/desci/dpid-resolver + # context: ~/dev/desci/dpid-resolver environment: DPID_ENV: local OPTIMISM_RPC_URL: http://host.docker.internal:8545 CERAMIC_URL: http://host.docker.internal:7007 IPFS_GATEWAY: http://host.docker.internal:8089/ipfs - REDIS_HOST: http://host.docker.internal + REDIS_HOST: host.docker.internal REDIS_PORT: 6379 # How long to store anchored commit info (default 24 hours) CACHE_TTL_ANCHORED: 86400 From 021b29fbfa014f137d207415815033d1a64b3b89 Mon Sep 17 00:00:00 2001 From: shadrach-tayo Date: Wed, 2 Oct 2024 06:37:41 -0500 Subject: [PATCH 6/6] add node version to dpid prefix in comment publish --- desci-server/src/controllers/nodes/publish.ts | 11 +++++++---- desci-server/src/services/Attestation.ts | 6 ++---- desci-server/src/services/fixDpid.ts | 1 + desci-server/src/theGraph.ts | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/desci-server/src/controllers/nodes/publish.ts b/desci-server/src/controllers/nodes/publish.ts index a63b08815..382d38c92 100644 --- a/desci-server/src/controllers/nodes/publish.ts +++ b/desci-server/src/controllers/nodes/publish.ts @@ -16,6 +16,7 @@ import { setDpidAlias, } from '../../services/nodeManager.js'; import { publishServices } from '../../services/PublishServices.js'; +import { getIndexedResearchObjects } from '../../theGraph.js'; import { discordNotify } from '../../utils/discordUtils.js'; import { ensureUuidEndsWithDot } from '../../utils.js'; @@ -70,7 +71,6 @@ export const publish = async (req: PublishRequest, res: Response body: req.body, uuid, cid, - manifest, transactionId, ceramicStream, commitId, @@ -165,15 +165,18 @@ export const publish = async (req: PublishRequest, res: Response where: { nodeId: node.id, root: true, userId: owner.id }, orderBy: { updatedAt: 'desc' }, }); - logger.info({ root }, 'publishDraftComments::Root'); + const result = await getIndexedResearchObjects([ensureUuidEndsWithDot(uuid)]); + // if node is being published for the first time default to 1 + const version = result ? result.researchObjects?.[0]?.versions.length : 1; + logger.info({ root, result, version }, 'publishDraftComments::Root'); + // publish draft comments await attestationService.publishDraftComments({ node, userId: owner.id, dpidAlias: dpidAlias ?? parseInt(manifest.dpid?.id), rootCid: root.rootCid, - // todo: get version number - version: 0, + version, }); return res.send({ diff --git a/desci-server/src/services/Attestation.ts b/desci-server/src/services/Attestation.ts index f7002350f..15d4a48a1 100644 --- a/desci-server/src/services/Attestation.ts +++ b/desci-server/src/services/Attestation.ts @@ -679,10 +679,10 @@ export class AttestationService { rootCid: string; }) { const dpidUrl = process.env.DPID_URL_OVERRIDE ?? 'https://beta.dpid.org'; - // todo: specify version here - const dpidPrefix = `${dpidUrl}/${dpidAlias}`; + const dpidPrefix = `${dpidUrl}/${dpidAlias}/v${version}`; const comments = await prisma.annotation.findMany({ where: { uuid: node.uuid, visible: false } }); + logger.info({ dpidPrefix, comments }, 'publishDraftComments'); const publishedComments = await asyncMap(comments, async (comment) => { const highlights = (comment.highlights.map((h) => JSON.parse(h as string)) ?? []) as HighlightBlock[]; @@ -702,8 +702,6 @@ export class AttestationService { return { ...highlight, path: transformedPath }; }); - logger.info({ highlights }, 'publishDraftComments::Highlights'); - logger.info({ transformed }, 'publishDraftComments::Transformed'); return { id: comment.id, highlights: transformed.map((h) => JSON.stringify(h)), diff --git a/desci-server/src/services/fixDpid.ts b/desci-server/src/services/fixDpid.ts index 4de297940..a3ee5690f 100644 --- a/desci-server/src/services/fixDpid.ts +++ b/desci-server/src/services/fixDpid.ts @@ -20,6 +20,7 @@ export const getTargetDpidUrl = () => { const TARGET_DPID_URL_BY_SERVER_URL = { 'https://nodes-api-dev.desci.com': 'https://dev-beta.dpid.org', 'https://nodes-api.desci.com': 'https://beta.dpid.org', + 'http://localhost:5420': 'http://host.docker.internal:5460', }; const targetDpidUrl = TARGET_DPID_URL_BY_SERVER_URL[process.env.SERVER_URL]; return targetDpidUrl; diff --git a/desci-server/src/theGraph.ts b/desci-server/src/theGraph.ts index 6fef11a88..a9e7ec8e9 100644 --- a/desci-server/src/theGraph.ts +++ b/desci-server/src/theGraph.ts @@ -10,7 +10,7 @@ const logger = parentLogger.child({ module: 'GetIndexedResearchObjects', }); -const RESOLVER_URL = process.env.DPID_URL_OVERRIDE || getTargetDpidUrl(); +const RESOLVER_URL = getTargetDpidUrl(); export type IndexedResearchObject = { /** Hex: Node UUID */