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 d44babc72..56e55a238 100755 --- a/desci-server/prisma/schema.prisma +++ b/desci-server/prisma/schema.prisma @@ -51,6 +51,7 @@ model Node { DoiSubmissionQueue DoiSubmissionQueue[] BookmarkedNode BookmarkedNode[] DeferredEmails DeferredEmails[] + Annotation Annotation[] @@index([ownerId]) @@index([uuid]) @@ -811,16 +812,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..343bdc472 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 && { 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 && { 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/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 body: req.body, uuid, cid, - manifest, transactionId, ceramicStream, commitId, @@ -160,6 +161,24 @@ export const publish = async (req: PublishRequest, 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' }, + }); + 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, + version, + }); + return res.send({ ok: true, dpid: dpidAlias ?? parseInt(manifest.dpid?.id), 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..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(); @@ -34,22 +36,53 @@ 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+.*)?/m; + 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) => { + logger.info({ uuidPathRegex: uuidPathRegex.source, dpidPathRegex: dpidPathRegex.source }, 'REGEX'); + return 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 +114,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 +126,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 +134,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 e25bac597..0e2e0c499 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'; @@ -581,21 +583,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 +612,8 @@ export class AttestationService { nodeAttestationId: claimId, body: comment, links, + uuid, + visible, }; return this.createAnnotation(data); } @@ -614,22 +624,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,10 +655,69 @@ export class AttestationService { body: comment, links, highlights: highlights.map((h) => JSON.stringify(h)), + uuid, + visible, }; 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'; + 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[]; + + 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 }; + }); + + 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 as number }, data: comment }), + ), + ); + } + async removeComment(commentId: number) { return prisma.annotation.delete({ where: { id: commentId }, @@ -728,6 +803,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/services/fixDpid.ts b/desci-server/src/services/fixDpid.ts index 389fec225..e6d01b7fd 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 as string; diff --git a/desci-server/src/theGraph.ts b/desci-server/src/theGraph.ts index 8da267532..0ab8d5027 100644 --- a/desci-server/src/theGraph.ts +++ b/desci-server/src/theGraph.ts @@ -11,7 +11,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 */ diff --git a/desci-server/test/integration/Attestation.test.ts b/desci-server/test/integration/Attestation.test.ts index a6a111899..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[]; @@ -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, }); }); @@ -2244,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); @@ -2254,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 }); @@ -2265,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, diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index a00e563b2..4630c3c39 100755 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -236,7 +236,7 @@ services: 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