import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; import { Hono } from "hono"; import { z } from "zod"; import { HTTPException } from "hono/http-exception"; import db from "../../drizzle"; import { questions } from "../../drizzle/schema/questions"; import HonoEnv from "../../types/HonoEnv"; import requestValidator from "../../utils/requestValidator"; import authInfo from "../../middlewares/authInfo"; import checkPermission from "../../middlewares/checkPermission"; import { aspects } from "../../drizzle/schema/aspects"; import { subAspects } from "../../drizzle/schema/subAspects"; import { notFound } from "../../errors/DashboardError"; export const questionFormSchema = z.object({ subAspectId: z.string().min(1).max(255), question: z.string().min(1).max(255), needFile: z.boolean().default(false), }); export const questionUpdateSchema = questionFormSchema.extend({ question: z.string().min(1).max(255).or(z.literal("")), subAspectId: z.string().min(1).max(255).or(z.literal("")), needFile: z.boolean().default(false).or(z.boolean()), }); const questionsRoute = new Hono() .use(authInfo) /** * Get All Questions (With Metadata) * * Query params: * - includeTrashed: boolean (default: false) * - withMetadata: boolean */ .get( "/", checkPermission("questions.readAll"), requestValidator( "query", z.object({ includeTrashed: z .string() .optional() .transform((v) => v?.toLowerCase() === "true"), withMetadata: z .string() .optional() .transform((v) => v?.toLowerCase() === "true"), page: z.coerce.number().int().min(0).default(0), limit: z.coerce.number().int().min(1).max(1000).default(30), q: z.string().default(""), }) ), async (c) => { const { includeTrashed, page, limit, q } = c.req.valid("query"); const totalCountQuery = includeTrashed ? sql`(SELECT count(*) FROM ${questions})` : sql`(SELECT count(*) FROM ${questions} WHERE ${questions.deletedAt} IS NULL)`; const result = await db .select({ id: questions.id, question: questions.question, needFile: questions.needFile, aspectName: aspects.name, subAspectName: subAspects.name, createdAt: questions.createdAt, updatedAt: questions.updatedAt, ...(includeTrashed ? { deletedAt: questions.deletedAt } : {}), fullCount: totalCountQuery, }) .from(questions) .leftJoin(subAspects, eq(questions.subAspectId, subAspects.id)) .leftJoin(aspects, eq(subAspects.aspectId, aspects.id)) .where( and( includeTrashed ? undefined : isNull(questions.deletedAt), q ? or( ilike(questions.createdAt, q), ilike(questions.updatedAt, q), ilike(questions.deletedAt, q), eq(questions.id, q) ) : undefined ) ) .offset(page * limit) .limit(limit); return c.json({ data: result.map((d) => ({ ...d, fullCount: undefined })), _metadata: { currentPage: page, totalPages: Math.ceil( (Number(result[0]?.fullCount) ?? 0) / limit ), totalItems: Number(result[0]?.fullCount) ?? 0, perPage: limit, }, }); } ) /** * Get Question by ID * * Query params: * - id: string * - includeTrashed: boolean (default: false) */ .get( "/:id", checkPermission("questions.readAll"), requestValidator( "query", z.object({ includeTrashed: z.string().default("false"), }) ), async (c) => { const questionId = c.req.param("id"); const includeTrashed = c.req.query("includeTrashed")?.toLowerCase() === "true"; const queryResult = await db .select({ id: questions.id, question: questions.question, needFile: questions.needFile, aspectId: aspects.id, subAspectId: subAspects.id, createdAt: questions.createdAt, updatedAt: questions.updatedAt, ...(includeTrashed ? { deletedAt: questions.deletedAt } : {}), }) .from(questions) .leftJoin(subAspects, eq(questions.subAspectId, subAspects.id)) .leftJoin(aspects, eq(subAspects.aspectId, aspects.id)) .where( and( eq(questions.id, questionId), !includeTrashed ? isNull(questions.deletedAt) : undefined ) ); if (!queryResult[0]) throw notFound(); const questionData = { ...queryResult[0], }; return c.json(questionData); } ) /** * Create Question * * JSON: * - questionFormSchema: object */ .post( "/", checkPermission("questions.create"), requestValidator("json", questionFormSchema), async (c) => { const questionData = c.req.valid("json"); const question = await db .insert(questions) .values({ question: questionData.question, needFile: questionData.needFile, subAspectId: questionData.subAspectId, }) .returning(); return c.json( { message: "Question created successfully", data: question, }, 201 ); } ) /** * Update Question * * JSON: * - questionUpdateSchema: object */ .patch( "/:id", checkPermission("questions.update"), requestValidator("json", questionUpdateSchema), async (c) => { const questionId = c.req.param("id"); const questionData = c.req.valid("json"); const question = await db .select() .from(questions) .where(and(eq(questions.id, questionId), isNull(questions.deletedAt))); if (!question[0]) throw notFound(); await db .update(questions) .set({ ...questionData, updatedAt: new Date(), }) .where(eq(questions.id, questionId)); return c.json({ message: "Question updated successfully", }); } ) /** * Delete Question * * Query params: * - id: string * - skipTrash: string (default: false) */ .delete( "/:id", checkPermission("questions.delete"), requestValidator( "query", z.object({ skipTrash: z.string().default("false"), }) ), async (c) => { const questionId = c.req.param("id"); const skipTrash = c.req.valid("query").skipTrash.toLowerCase() === "true"; const question = await db .select() .from(questions) .where( and( eq(questions.id, questionId), skipTrash ? undefined : isNull(questions.deletedAt) ) ); if (!question[0]) throw notFound(); if (skipTrash) { await db.delete(questions).where(eq(questions.id, questionId)); } else { await db .update(questions) .set({ deletedAt: new Date(), }) .where(and(eq(questions.id, questionId), isNull(questions.deletedAt))); } return c.json({ message: "Question deleted successfully", }); } ) /** * Restore Question * * Query params: * - id: string */ .patch("/restore/:id", checkPermission("questions.restore"), async (c) => { const questionId = c.req.param("id"); const question = ( await db.select().from(questions).where(eq(questions.id, questionId)) )[0]; if (!question) throw notFound(); if (!question.deletedAt) { throw new HTTPException(400, { message: "The question is not deleted", }); } await db .update(questions) .set({ deletedAt: null }) .where(eq(questions.id, questionId)); return c.json({ message: "Question restored successfully", }); }); export default questionsRoute;