- Clone import: defer FurnitureData.json writes and append all entries in a single batched write instead of one read-modify-write per item, removing the main serialization bottleneck for large batches. - Clone import: raise SSE batch concurrency cap from 5 to 10 and bump the clone client/route default from 2 to 6. - Add a flush hook to runSseBatch so callers can batch deferred work before batch_complete is emitted, and surface flush errors as an error event. - Enable Next.js Cache Components (instant: false opt-out) and silence the related build warnings in next.config.ts. - Switch isomorphic-dompurify to dompurify and refresh dependencies.
91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
// Public REST API — a single help-center ticket (with replies).
|
|
//
|
|
// Bearer-authed. GET returns one ticket that MUST belong to the authed user,
|
|
// together with its replies; reply author usernames are resolved in a single
|
|
// users lookup. Fail-soft: never a 500.
|
|
|
|
import { asc, eq, inArray } from "drizzle-orm";
|
|
import { apiError, apiJson, positiveBigInt } from "@/lib/api";
|
|
import { bearerUserId } from "@/lib/api-auth";
|
|
import {
|
|
db,
|
|
User,
|
|
WebsiteHelpCenterTicketReplies,
|
|
WebsiteHelpCenterTickets,
|
|
} from "@/lib/db";
|
|
|
|
// GET /api/tickets/:id
|
|
export async function GET(
|
|
req: Request,
|
|
{ params }: { params: Promise<{ id: string }> },
|
|
) {
|
|
const uid = await bearerUserId(req);
|
|
if (!uid) return apiError("Unauthorized", 401);
|
|
|
|
const { id } = await params;
|
|
const ticketId = positiveBigInt(id);
|
|
if (!ticketId) return apiError("Invalid ticket id", 422);
|
|
|
|
try {
|
|
const [ticket] = await db
|
|
.select({
|
|
id: WebsiteHelpCenterTickets.id,
|
|
userId: WebsiteHelpCenterTickets.userId,
|
|
categoryId: WebsiteHelpCenterTickets.categoryId,
|
|
title: WebsiteHelpCenterTickets.title,
|
|
content: WebsiteHelpCenterTickets.content,
|
|
open: WebsiteHelpCenterTickets.open,
|
|
createdAt: WebsiteHelpCenterTickets.createdAt,
|
|
})
|
|
.from(WebsiteHelpCenterTickets)
|
|
.where(eq(WebsiteHelpCenterTickets.id, ticketId))
|
|
.limit(1);
|
|
|
|
// Ownership check — return 404 (not 403) so a foreign id is indistinguishable
|
|
// from a missing one.
|
|
if (!ticket || ticket.userId !== uid)
|
|
return apiError("Ticket not found", 404);
|
|
|
|
const replies = await db
|
|
.select({
|
|
id: WebsiteHelpCenterTicketReplies.id,
|
|
userId: WebsiteHelpCenterTicketReplies.userId,
|
|
content: WebsiteHelpCenterTicketReplies.content,
|
|
createdAt: WebsiteHelpCenterTicketReplies.createdAt,
|
|
})
|
|
.from(WebsiteHelpCenterTicketReplies)
|
|
.where(eq(WebsiteHelpCenterTicketReplies.ticketId, ticketId))
|
|
.orderBy(asc(WebsiteHelpCenterTicketReplies.id));
|
|
|
|
// Resolve author usernames in one query.
|
|
const authorIds = [...new Set(replies.map((r) => r.userId))];
|
|
const authors = authorIds.length
|
|
? await db
|
|
.select({ id: User.id, username: User.username })
|
|
.from(User)
|
|
.where(inArray(User.id, authorIds))
|
|
: [];
|
|
const nameById = new Map(authors.map((a) => [a.id, a.username]));
|
|
|
|
return apiJson({
|
|
ticket: {
|
|
id: ticket.id,
|
|
categoryId: ticket.categoryId,
|
|
title: ticket.title,
|
|
content: ticket.content,
|
|
open: ticket.open,
|
|
createdAt: ticket.createdAt,
|
|
replies: replies.map((r) => ({
|
|
id: r.id,
|
|
userId: r.userId,
|
|
username: nameById.get(r.userId) ?? null,
|
|
content: r.content,
|
|
createdAt: r.createdAt,
|
|
})),
|
|
},
|
|
});
|
|
} catch {
|
|
return apiError("Failed to load ticket", 503);
|
|
}
|
|
}
|