Files
jarv.is/lib/server/comments.ts
jake d3b2d07de7 refactor: use "use cache: remote" for request-time reads and updateTag/refresh for mutations
- Replace `"use cache"` with `"use cache: remote"` on functions that run inside dynamic holes (`getContributions`, `getRepos`, `getPostStats`, `getCachedComments`) so entries are shared across server instances
- Switch comment and view server actions from `revalidatePath`/`revalidateTag` to `updateTag` + `refresh` for read-your-writes invalidation
- Extract a `commentsTag(pageSlug)` helper so each page's comment cache is expired independently instead of blowing the whole "comments" tag
- Refactor `getComments` to delegate to a cached inner function that throws on failure (so errors are never cached); `getCommentCount` now reuses that cache entry instead of issuing a separate COUNT query
- Drop `lib/data/views.ts` (`getAllViewCounts`) — the `/api/hits` route now calls `getPostStats()` instead
- Remove `revalidateTag("views")` from `incrementViews`; view counts in `getPostStats` are allowed to lag by a cache TTL
- Remove the `Suspense`/`Skeleton` wrapper on the projects page since `getContributions` is now a remote-cached call
2026-10-03 09:50:06 -04:00

178 lines
5.0 KiB
TypeScript

"use server";
import { eq } from "drizzle-orm";
import { refresh, updateTag } from "next/cache";
import { headers } from "next/headers";
import { z } from "zod";
import { auth } from "@/lib/auth";
import { commentsTag } from "@/lib/data/comments";
import { db } from "@/lib/db";
import * as schema from "@/lib/db/schema";
import { getPostByPageSlug } from "@/lib/posts";
const commentContentSchema = z.string().trim().min(1).max(10_000);
const commentIdSchema = z.uuid();
const createCommentSchema = z.object({
content: commentContentSchema,
pageSlug: z.string(),
parentId: commentIdSchema.optional(),
});
export const createComment = async (data: {
content: string;
pageSlug: string;
parentId?: string;
}) => {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session || !session.user) {
throw new Error("You must be logged in to comment");
}
const parsed = createCommentSchema.safeParse(data);
if (!parsed.success) {
throw new Error("Invalid comment");
}
const { content, pageSlug, parentId } = parsed.data;
// only allow comments on existing posts that haven't disabled them
const post = getPostByPageSlug(pageSlug);
if (!post || post.noComments) {
throw new Error("Comments are closed for this page");
}
try {
// Replies must point at an existing comment on the same page
if (parentId) {
const parent = await db
.select({ pageSlug: schema.comment.pageSlug })
.from(schema.comment)
.where(eq(schema.comment.id, parentId))
.then((results) => results[0]);
if (!parent || parent.pageSlug !== pageSlug) {
throw new Error("Parent comment not found");
}
}
// Insert the comment
await db.insert(schema.comment).values({
content,
pageSlug,
parentId: parentId ?? null,
userId: session.user.id,
});
// Expire this page's cached comments and the post stats, then refresh the commenter's view
updateTag(commentsTag(pageSlug));
updateTag("comments");
refresh();
} catch (error) {
console.error("[server/comments] error creating comment:", error);
throw new Error("Failed to create comment", { cause: error });
}
};
export const updateComment = async (commentId: string, content: string) => {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session || !session.user) {
throw new Error("You must be logged in to update a comment");
}
const parsedId = commentIdSchema.safeParse(commentId);
const parsedContent = commentContentSchema.safeParse(content);
if (!parsedId.success || !parsedContent.success) {
throw new Error("Invalid comment");
}
try {
// Get the comment to verify ownership
const comment = await db
.select({
userId: schema.comment.userId,
pageSlug: schema.comment.pageSlug,
})
.from(schema.comment)
.where(eq(schema.comment.id, commentId))
.then((results) => results[0]);
if (!comment) {
throw new Error("Comment not found");
}
// Verify ownership
if (comment.userId !== session.user.id) {
throw new Error("You can only edit your own comments");
}
// Update the comment
await db
.update(schema.comment)
.set({
content: parsedContent.data,
updatedAt: new Date(),
})
.where(eq(schema.comment.id, commentId));
// Expire this page's cached comments, then refresh the commenter's view
updateTag(commentsTag(comment.pageSlug));
refresh();
} catch (error) {
console.error("[server/comments] error updating comment:", error);
throw new Error("Failed to update comment", { cause: error });
}
};
export const deleteComment = async (commentId: string) => {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session || !session.user) {
throw new Error("You must be logged in to delete a comment");
}
if (!commentIdSchema.safeParse(commentId).success) {
throw new Error("Invalid comment ID");
}
try {
// Get the comment to verify ownership and get the page_slug for revalidation
const comment = await db
.select({
userId: schema.comment.userId,
pageSlug: schema.comment.pageSlug,
})
.from(schema.comment)
.where(eq(schema.comment.id, commentId))
.then((results) => results[0]);
if (!comment) {
throw new Error("Comment not found");
}
// Verify ownership
if (comment.userId !== session.user.id) {
throw new Error("You can only delete your own comments");
}
// Delete the comment
await db.delete(schema.comment).where(eq(schema.comment.id, commentId));
// Expire this page's cached comments and the post stats, then refresh the commenter's view
updateTag(commentsTag(comment.pageSlug));
updateTag("comments");
refresh();
} catch (error) {
console.error("[server/comments] error deleting comment:", error);
throw new Error("Failed to delete comment", { cause: error });
}
};