Skip to content

Commit

Permalink
Merge pull request #788 from desci-labs/edit-comments
Browse files Browse the repository at this point in the history
feat: edit comments
  • Loading branch information
shadrach-tayo authored Jan 28, 2025
2 parents 4447841 + 3d8adbe commit 7cfadf6
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 10 deletions.
1 change: 1 addition & 0 deletions desci-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"axios": "^1.3.5",
"axios-retry": "^4.5.0",
"body-parser": "^1.20.2",
"chai-as-promised": "^8.0.1",
"concurrently": "^8.2.0",
"cookie-parser": "^1.4.5",
"cron": "^3.1.7",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ActionType" ADD VALUE 'EDIT_COMMENT';
2 changes: 1 addition & 1 deletion desci-server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,7 @@ enum ActionType {
UPDATE_ORCID_RECORD
REMOVE_ORCID_WORK_RECORD
ORCID_API_ERROR
// AUTOMATE_METADATA
EDIT_COMMENT
}

enum ChainTransactionType {
Expand Down
65 changes: 63 additions & 2 deletions desci-server/src/controllers/nodes/comments.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { VoteType } from '@prisma/client';
import { ActionType, VoteType } from '@prisma/client';
import { Response, NextFunction } from 'express';
import z from 'zod';

import { prisma } from '../../client.js';
import { NotFoundError } from '../../core/ApiError.js';
import { SuccessMessageResponse, SuccessResponse } from '../../core/ApiResponse.js';
import { logger } from '../../logger.js';
import { RequestWithNode, RequestWithUser } from '../../middleware/authorisation.js';
import { getCommentsSchema, postCommentVoteSchema } from '../../routes/v1/attestations/schema.js';
import { editCommentsSchema, getCommentsSchema, postCommentVoteSchema } from '../../routes/v1/attestations/schema.js';
import { attestationService } from '../../services/Attestation.js';
import { saveInteraction } from '../../services/interactionLog.js';
import { asyncMap, ensureUuidEndsWithDot } from '../../utils.js';

const parentLogger = logger.child({ module: 'Comments' });
export const getGeneralComments = async (req: RequestWithNode, res: Response, _next: NextFunction) => {
const { uuid } = req.params as z.infer<typeof getCommentsSchema>['params'];
const { cursor, limit } = req.query as z.infer<typeof getCommentsSchema>['query'];
Expand Down Expand Up @@ -51,6 +54,64 @@ export const getGeneralComments = async (req: RequestWithNode, res: Response, _n
return new SuccessResponse({ cursor: nextCursor, count, comments }).send(res);
};

export const editComment = async (req: RequestWithUser, res: Response) => {
const { id } = req.body as z.infer<typeof editCommentsSchema>['params'];
const { links, body } = req.body as z.infer<typeof editCommentsSchema>['body'];

const user = req.user;

const logger = parentLogger.child({
commentId: req.params.id,
module: 'Comments::Edit',
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(`EditComment`);

// let comment = await attestationService.getComment({ id });
// if (!comment) throw new NotFoundError();

// if (comment.authorId !== user.id) throw new ForbiddenError();
const comment = await attestationService.editComment({ authorId: req.user.id, id, update: { body, links } });
// if (highlights?.length > 0) {
// const processedHighlights = await asyncMap(highlights, async (highlight) => {
// if (!('image' in highlight)) return highlight;
// const blob = base64ToBlob(highlight.image);
// const storedCover = await client.add(blob, { cidVersion: 1 });

// return { ...highlight, image: `${PUBLIC_IPFS_PATH}/${storedCover.cid}` };
// });
// logger.info({ processedHighlights }, 'processedHighlights');
// annotation = await attestationService.createHighlight({
// 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: claimId && parseInt(claimId.toString()),
// authorId: user.id,
// comment: body,
// links,
// visible,
// ...(uuid && { uuid: ensureUuidEndsWithDot(uuid) }),
// });
// }
await saveInteraction(req, ActionType.EDIT_COMMENT, { commentId: comment.id });
// await emitNotificationForAnnotation(annotation.id);
new SuccessResponse(comment).send(res);
};

export const upvoteComment = async (req: RequestWithUser, res: Response, _next: NextFunction) => {
const { uuid, commentId } = req.params as z.infer<typeof postCommentVoteSchema>['params'];
const node = await prisma.node.findFirst({ where: { uuid: ensureUuidEndsWithDot(uuid) } });
Expand Down
11 changes: 11 additions & 0 deletions desci-server/src/routes/v1/attestations/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ export const getCommentsSchema = z.object({
}),
});

export const editCommentsSchema = z.object({
params: z.object({
// quickly disqualify false uuid strings
id: z.coerce.number(),
}),
body: z.object({
body: z.string(),
links: z.array(z.string()).optional(),
}),
});

export const postCommentVoteSchema = z.object({
params: z.object({
// quickly disqualify false uuid strings
Expand Down
9 changes: 8 additions & 1 deletion desci-server/src/routes/v1/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
getUserVote,
deleteUserVote,
downvoteComment,
editComment,
} from '../../controllers/nodes/index.js';
import { retrieveTitle } from '../../controllers/nodes/legacyManifestApi.js';
import { preparePublishPackage } from '../../controllers/nodes/preparePublishPackage.js';
Expand All @@ -79,7 +80,12 @@ import { ensureUser } from '../../middleware/permissions.js';
import { validate } from '../../middleware/validator.js';
import { asyncHandler } from '../../utils/asyncHandler.js';

import { getCommentsSchema, postCommentVoteSchema, showNodeAttestationsSchema } from './attestations/schema.js';
import {
editCommentsSchema,
getCommentsSchema,
postCommentVoteSchema,
showNodeAttestationsSchema,
} from './attestations/schema.js';

const router = Router();

Expand Down Expand Up @@ -188,6 +194,7 @@ router.post(
[ensureUser, validate(postCommentVoteSchema)],
asyncHandler(upvoteComment),
);
router.put('/comments/:id', [ensureUser, validate(editCommentsSchema)], asyncHandler(editComment));
router.post(
'/:uuid/comments/:commentId/downvote',
[ensureUser, validate(postCommentVoteSchema)],
Expand Down
51 changes: 50 additions & 1 deletion desci-server/src/services/Attestation.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import assert from 'assert';

import { HighlightBlock } from '@desci-labs/desci-models';
import { AnnotationType, Attestation, AttestationVersion, Node, Prisma, User, VoteType } from '@prisma/client';
import {
Annotation,
AnnotationType,
Attestation,
AttestationVersion,
Node,
Prisma,
User,
VoteType,
} from '@prisma/client';
import sgMail from '@sendgrid/mail';
import _ from 'lodash';

import { prisma } from '../client.js';
import { ForbiddenError } from '../core/ApiError.js';
import {
AttestationNotFoundError,
AttestationVersionNotFoundError,
ClaimNotFoundError,
CommentNotFoundError,
CommunityNotFoundError,
DuplicateClaimError,
DuplicateDataError,
Expand Down Expand Up @@ -682,6 +693,38 @@ export class AttestationService {
return this.createAnnotation(data);
}

async editComment({
update: { body, links },
id,
authorId,
}: {
update: { body: string; links?: string[] };
id: number;
authorId: number;
}) {
const comment = await prisma.annotation.findFirst({ where: { id } });
if (!comment) throw new CommentNotFoundError();

if (comment.authorId !== authorId) throw new ForbiddenError();

if (comment.nodeAttestationId) {
const claim = await this.findClaimById(comment.nodeAttestationId);
if (!claim) throw new ClaimNotFoundError();

const attestation = await this.findAttestationById(claim.attestationId);
if (attestation.protected) {
await this.assertUserIsMember(comment.authorId, attestation.communityId);
}
}

const data: Prisma.AnnotationUpdateInput = {
type: AnnotationType.COMMENT,
body,
links: links || comment.links,
};
return prisma.annotation.update({ where: { id: comment.id }, data });
}

async createHighlight({
claimId,
authorId,
Expand Down Expand Up @@ -901,6 +944,12 @@ export class AttestationService {
});
}

async getComment(filter: Prisma.AnnotationWhereInput) {
return prisma.annotation.findFirst({
where: filter,
});
}

async countComments(filter: Prisma.AnnotationWhereInput) {
return prisma.annotation.count({
where: filter,
Expand Down
62 changes: 57 additions & 5 deletions desci-server/test/integration/Attestation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import {
User,
VoteType,
} from '@prisma/client';
import { assert, expect } from 'chai';
import chai, { assert } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import jwt from 'jsonwebtoken';
import request from 'supertest';

import { prisma } from '../../src/client.js';
import { NodeAttestationFragment } from '../../src/controllers/attestations/show.js';
import { Engagement, NodeRadar, NodeRadarEntry, NodeRadarItem } from '../../src/controllers/communities/types.js';
import { ForbiddenError } from '../../src/core/ApiError.js';
import {
DuplicateReactionError,
DuplicateVerificationError,
Expand All @@ -38,6 +40,10 @@ import { client as ipfs, spawnEmptyManifest } from '../../src/services/ipfs.js';
import { randomUUID64 } from '../../src/utils.js';
import { createDraftNode, createUsers } from '../util.js';

// use async chai assertions
chai.use(chaiAsPromised);
const expect = chai.expect;

const communitiesData = [
{
name: 'Desci Labs',
Expand Down Expand Up @@ -132,7 +138,7 @@ const clearDatabase = async () => {
await prisma.$queryRaw`TRUNCATE TABLE "Node" CASCADE;`;
};

describe('Attestations Service', async () => {
describe.only('Attestations Service', async () => {
let baseManifest: ResearchObjectV1;
let baseManifestCid: string;
let users: User[];
Expand Down Expand Up @@ -832,7 +838,7 @@ describe('Attestations Service', async () => {
});
});

describe('Annotations(Comments)', async () => {
describe.only('Annotations(Comments)', async () => {
let claim: NodeAttestation;
let node: Node;
const nodeVersion = 0;
Expand Down Expand Up @@ -886,6 +892,52 @@ describe('Attestations Service', async () => {
expect(voidComment.length).to.be.equal(0);
expect(voidComment[0]).to.be.undefined;
});

it('should edit a comment', async () => {
comment = await attestationService.createComment({
links: [],
claimId: claim.id,
authorId: users[1].id,
comment: 'Old comment to be edited',
visible: true,
});

const editedComment = await attestationService.editComment({
update: { body: 'edited comment', links: ['https://google.com'] },
authorId: users[1].id,
id: comment.id,
});
expect(editedComment.body).to.be.equal('edited comment');
expect(editedComment.links[0]).to.be.equal('https://google.com');
});

it('should not allow another author to edit a comment', async () => {
try {
await attestationService.editComment({
update: { body: 'edited comment', links: ['https://google.com'] },
authorId: users[2].id,
id: comment.id,
});
} catch (error) {
expect(error).to.be.instanceOf(ForbiddenError);
}
});

it('should edit a comment(via api)', async () => {
const commenterJwtToken = jwt.sign({ email: users[1].email }, process.env.JWT_SECRET!, {
expiresIn: '1y',
});
const commenterJwtHeader = `Bearer ${commenterJwtToken}`;
const res = await request(app)
.put(`/v1/nodes/comments/${comment.id}`)
.set('authorization', commenterJwtHeader)
.send({ body: 'edit comment via api', links: ['https://desci.com'] });
expect(res.statusCode).to.equal(200);
const editedComment = (await res.body.data) as Annotation;

expect(editedComment.body).to.be.equal('edit comment via api');
expect(editedComment.links[0]).to.be.equal('https://desci.com');
});
});

describe('Node Attestation Verification', async () => {
Expand Down Expand Up @@ -2328,7 +2380,7 @@ describe('Attestations Service', async () => {
});
});

describe.only('Annotations(Comments) Vote', async () => {
describe('Annotations(Comments) Vote', async () => {
let claim: NodeAttestation;
let node: Node;
const nodeVersion = 0;
Expand Down Expand Up @@ -2506,7 +2558,7 @@ describe('Attestations Service', async () => {
await prisma.commentVote.deleteMany({});
});

it.only('should test user upvote via api', async () => {
it('should test user upvote via api', async () => {
const voterJwtToken = jwt.sign({ email: voter.email }, process.env.JWT_SECRET!, {
expiresIn: '1y',
});
Expand Down
12 changes: 12 additions & 0 deletions desci-server/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8917,6 +8917,13 @@ cborg@^4.0.5, cborg@^4.0.8:
resolved "https://registry.yarnpkg.com/cborg/-/cborg-4.2.1.tgz#57170ef570dcdaf93575469a51f3b918a854669d"
integrity sha512-LSdnRagOTx1QZ3/ECLEOMc5fYHaDBjjQkBeBGtZ9KkGa78Opb5UzUxJeuxhmYTZm1DUzdBjj9JT3fcQNRL9ZBg==

chai-as-promised@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-8.0.1.tgz#8913b2fd685b3f5637d25f627518e3ac9614d8e1"
integrity sha512-OIEJtOL8xxJSH8JJWbIoRjybbzR52iFuDHuF8eb+nTPD6tgXLjRqsgnUGqQfFODxYvq5QdirT0pN9dZ0+Gz6rA==
dependencies:
check-error "^2.0.0"

chai@^4.3.4:
version "4.4.1"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1"
Expand Down Expand Up @@ -8965,6 +8972,11 @@ check-error@^1.0.3:
dependencies:
get-func-name "^2.0.2"

check-error@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==

[email protected]:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
Expand Down

0 comments on commit 7cfadf6

Please sign in to comment.