Pull Request branch dev-clone to main #1

Merged
gitea merged 429 commits from dev-clone into main 2024-12-23 09:31:34 +00:00
4 changed files with 315 additions and 0 deletions
Showing only changes of commit 8e03b61e1d - Show all commits

View File

@ -32,6 +32,21 @@ const permissionsData = [
{ {
code: "roles.delete", code: "roles.delete",
}, },
{
code: "questions.readAll",
},
{
code: "questions.create",
},
{
code: "questions.update",
},
{
code: "questions.delete",
},
{
code: "questions.restore",
},
] as const; ] as const;
export type SpecificPermissionCode = (typeof permissionsData)[number]["code"]; export type SpecificPermissionCode = (typeof permissionsData)[number]["code"];

View File

@ -14,6 +14,13 @@ const sidebarMenus: SidebarMenu[] = [
link: "/users", link: "/users",
color: "red", color: "red",
}, },
{
label: "Manajemen Pertanyaan",
icon: { tb: "TbChecklist" },
allowedPermissions: ["permissions.read"],
link: "/questions",
color: "green",
},
]; ];
export default sidebarMenus; export default sidebarMenus;

View File

@ -15,6 +15,7 @@ import DashboardError from "./errors/DashboardError";
import HonoEnv from "./types/HonoEnv"; import HonoEnv from "./types/HonoEnv";
import devRoutes from "./routes/dev/route"; import devRoutes from "./routes/dev/route";
import appEnv from "./appEnv"; import appEnv from "./appEnv";
import questionsRoute from "./routes/questions/route";
configDotenv(); configDotenv();
@ -78,6 +79,7 @@ const routes = app
.route("/dashboard", dashboardRoutes) .route("/dashboard", dashboardRoutes)
.route("/roles", rolesRoute) .route("/roles", rolesRoute)
.route("/dev", devRoutes) .route("/dev", devRoutes)
.route("/questions", questionsRoute)
.onError((err, c) => { .onError((err, c) => {
if (err instanceof DashboardError) { if (err instanceof DashboardError) {
return c.json( return c.json(

View File

@ -0,0 +1,291 @@
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";
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).optional().or(z.literal("")),
subAspectiD: z.string().min(1).max(255).optional().or(z.literal("")),
needFile: z.boolean().default(false).optional().or(z.boolean()),
});
const questionsRoute = new Hono<HonoEnv>()
.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<number>`(SELECT count(*) FROM ${questions})`
: sql<number>`(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(subAspects.aspectId, q),
ilike(questions.subAspectId, q),
ilike(questions.question, q),
ilike(questions.needFile, q),
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 user by id
.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,
aspectName: aspects.name,
subAspectName: subAspects.name,
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.length)
throw new HTTPException(404, {
message: "The question does not exists",
});
const userData = {
...queryResult[0],
};
return c.json(userData);
}
)
// create question
.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
.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]) return c.notFound();
await db
.update(questions)
.set({
...questionData,
updatedAt: new Date(),
})
.where(eq(questions.id, questionId));
return c.json({
message: "Question updated successfully",
});
}
)
//delete user
.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 new HTTPException(404, {
message: "The question is not found",
});
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",
});
}
)
// undo delete
.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) return c.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;