Files
sexy/packages/backend/src/graphql/resolvers/comments.ts
Sebastian Krüger 79932157bf
All checks were successful
Build and Push Backend Image / build (push) Successful in 43s
fix: revoke points when a comment is deleted
- revokePoints now accepts optional recordingId; when absent it deletes
  one matching row (for actions like COMMENT_CREATE that have no recording)
- deleteComment queues revokePoints + checkAchievements so leaderboard
  and social achievements stay in sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 11:16:18 +01:00

166 lines
4.8 KiB
TypeScript

import { GraphQLError } from "graphql";
import { builder } from "../builder";
import { CommentType, AdminCommentListType } from "../types/index";
import { comments, users } from "../../db/schema/index";
import { eq, and, desc, ilike, count } from "drizzle-orm";
import { requireOwnerOrAdmin, requireAdmin } from "../../lib/acl";
import { gamificationQueue } from "../../queues/index";
builder.queryField("commentsForVideo", (t) =>
t.field({
type: [CommentType],
args: {
videoId: t.arg.string({ required: true }),
},
resolve: async (_root, args, ctx) => {
const commentList = await ctx.db
.select()
.from(comments)
.where(and(eq(comments.collection, "videos"), eq(comments.item_id, args.videoId)))
.orderBy(desc(comments.date_created));
return Promise.all(
commentList.map(async (c) => {
const user = await ctx.db
.select({
id: users.id,
first_name: users.first_name,
last_name: users.last_name,
artist_name: users.artist_name,
avatar: users.avatar,
})
.from(users)
.where(eq(users.id, c.user_id))
.limit(1);
return { ...c, user: user[0] || null };
}),
);
},
}),
);
builder.mutationField("createCommentForVideo", (t) =>
t.field({
type: CommentType,
args: {
videoId: t.arg.string({ required: true }),
comment: t.arg.string({ required: true }),
},
resolve: async (_root, args, ctx) => {
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
const newComment = await ctx.db
.insert(comments)
.values({
collection: "videos",
item_id: args.videoId,
comment: args.comment,
user_id: ctx.currentUser.id,
})
.returning();
await gamificationQueue.add("awardPoints", {
job: "awardPoints",
userId: ctx.currentUser.id,
action: "COMMENT_CREATE",
});
await gamificationQueue.add("checkAchievements", {
job: "checkAchievements",
userId: ctx.currentUser.id,
category: "social",
});
const user = await ctx.db
.select({
id: users.id,
first_name: users.first_name,
last_name: users.last_name,
artist_name: users.artist_name,
avatar: users.avatar,
})
.from(users)
.where(eq(users.id, ctx.currentUser.id))
.limit(1);
return { ...newComment[0], user: user[0] || null };
},
}),
);
builder.mutationField("deleteComment", (t) =>
t.field({
type: "Boolean",
args: {
id: t.arg.int({ required: true }),
},
resolve: async (_root, args, ctx) => {
const comment = await ctx.db.select().from(comments).where(eq(comments.id, args.id)).limit(1);
if (!comment[0]) throw new GraphQLError("Comment not found");
requireOwnerOrAdmin(ctx, comment[0].user_id);
await ctx.db.delete(comments).where(eq(comments.id, args.id));
await gamificationQueue.add("revokePoints", {
job: "revokePoints",
userId: comment[0].user_id,
action: "COMMENT_CREATE",
});
await gamificationQueue.add("checkAchievements", {
job: "checkAchievements",
userId: comment[0].user_id,
category: "social",
});
return true;
},
}),
);
builder.queryField("adminListComments", (t) =>
t.field({
type: AdminCommentListType,
args: {
search: t.arg.string(),
limit: t.arg.int(),
offset: t.arg.int(),
},
resolve: async (_root, args, ctx) => {
requireAdmin(ctx);
const limit = args.limit ?? 50;
const offset = args.offset ?? 0;
const conditions = args.search ? [ilike(comments.comment, `%${args.search}%`)] : [];
const where = conditions.length > 0 ? and(...conditions) : undefined;
const [commentList, totalRows] = await Promise.all([
ctx.db
.select()
.from(comments)
.where(where)
.orderBy(desc(comments.date_created))
.limit(limit)
.offset(offset),
ctx.db.select({ total: count() }).from(comments).where(where),
]);
const items = await Promise.all(
commentList.map(async (c) => {
const user = await ctx.db
.select({
id: users.id,
first_name: users.first_name,
last_name: users.last_name,
artist_name: users.artist_name,
avatar: users.avatar,
})
.from(users)
.where(eq(users.id, c.user_id))
.limit(1);
return { ...c, user: user[0] || null };
}),
);
return { items, total: totalRows[0]?.total ?? 0 };
},
}),
);