add testimony api

This commit is contained in:
ItsMalma
2025-12-08 10:49:28 +07:00
parent a5794e9a1e
commit 0887b7c94b
7 changed files with 308 additions and 0 deletions

View File

@@ -38,6 +38,8 @@ import { PartnerMapper } from "@/modules/partner/partner.mapper";
import { StaticController } from "@/modules/static/static.controller";
import { TagController } from "@/modules/tag/tag.controller";
import { TagMapper } from "@/modules/tag/tag.mapper";
import { TestimonyController } from "@/modules/testimony/testimony.controller";
import { TestimonyMapper } from "@/modules/testimony/testimony.mapper";
import { TransportationClassController } from "@/modules/transportation-class/transportation-class.controller";
import { TransportationClassMapper } from "@/modules/transportation-class/transportation-class.mapper";
import { TransportationController } from "@/modules/transportation/transportation.controller";
@@ -100,6 +102,7 @@ export class Application {
);
const adminMapper = new AdminMapper();
const orderMapper = new OrderMapper(packageMapper, partnerMapper);
const testimonyMapper = new TestimonyMapper();
const tagMapper = new TagMapper();
const articleMapper = new ArticleMapper(tagMapper);
@@ -170,6 +173,10 @@ export class Application {
const whatsAppRouter = new WhatsAppController(
this._whatsAppService,
).buildRouter();
const testimonyRouter = new TestimonyController(
testimonyMapper,
this._jwtService,
).buildRouter();
const tagRouter = new TagController(
tagMapper,
this._jwtService,
@@ -196,6 +203,7 @@ export class Application {
this._app.use("/orders", orderRouter);
this._app.use("/statics", staticRouter);
this._app.use("/whatsapp", whatsAppRouter);
this._app.use("/testimonies", testimonyRouter);
this._app.use("/tags", tagRouter);
this._app.use("/articles", articleRouter);
}

View File

@@ -0,0 +1,29 @@
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
@Entity()
export class Testimony {
@PrimaryKey({ type: "varchar", length: 30 })
id!: string;
@Property({ type: "varchar", length: 200 })
review!: string;
@Property({ type: "varchar", length: 100 })
reviewer!: string;
@Property({ type: "int" })
rating!: number;
@Property({
type: "timestamp",
onCreate: () => new Date(),
})
createdAt!: Date;
@Property({
type: "timestamp",
onCreate: () => new Date(),
onUpdate: () => new Date(),
})
updatedAt!: Date;
}

View File

@@ -55,6 +55,10 @@ export enum AdminPermission {
createPartner = "partner:create",
updatePartner = "partner:update",
deletePartner = "partner:delete",
// Testimony permissions
createTestimony = "testimony:create",
updateTestimony = "testimony:update",
deleteTestimony = "testimony:delete",
// Tag permissions
createTag = "tag:create",
updateTag = "tag:update",

View File

@@ -0,0 +1,210 @@
import { Controller } from "@/common/controller";
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
import { paginationQuerySchema } from "@/common/schemas";
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
import type {
ErrorResponse,
ListResponse,
SingleResponse,
} from "@/common/types";
import { Testimony } from "@/database/entities/testimony.entity";
import { AdminPermission } from "@/database/enums/admin-permission.enum";
import { orm } from "@/database/orm";
import type { TestimonyMapper } from "@/modules/testimony/testimony.mapper";
import {
testimonyParamsSchema,
testimonyRequestSchema,
} from "@/modules/testimony/testimony.schemas";
import type { TestimonyResponse } from "@/modules/testimony/testimony.types";
import { Router, type Request, type Response } from "express";
import { ulid } from "ulid";
export class TestimonyController extends Controller {
public constructor(
private readonly mapper: TestimonyMapper,
private readonly jwtService: AbstractJwtService,
) {
super();
}
async create(req: Request, res: Response) {
const parseBodyResult = testimonyRequestSchema.safeParse(req.body);
if (!parseBodyResult.success) {
return this.handleZodError(parseBodyResult.error, res, "body");
}
const body = parseBodyResult.data;
const testimony = orm.em.create(Testimony, {
id: ulid(),
review: body.review,
reviewer: body.reviewer,
rating: body.rating,
createdAt: new Date(),
updatedAt: new Date(),
});
await orm.em.flush();
return res.status(201).json({
data: this.mapper.mapEntityToResponse(testimony),
errors: null,
} satisfies SingleResponse<TestimonyResponse>);
}
async list(req: Request, res: Response) {
const parseQueryResult = paginationQuerySchema.safeParse(req.query);
if (!parseQueryResult.success) {
return this.handleZodError(parseQueryResult.error, res, "query");
}
const query = parseQueryResult.data;
const count = await orm.em.count(Testimony);
const testimonies = await orm.em.find(
Testimony,
{},
{
limit: query.per_page,
offset: (query.page - 1) * query.per_page,
orderBy: { createdAt: "DESC" },
},
);
return res.status(200).json({
data: testimonies.map(this.mapper.mapEntityToResponse.bind(this.mapper)),
errors: null,
meta: {
page: query.page,
per_page: query.per_page,
total_pages: Math.ceil(count / query.per_page),
total_items: count,
},
} satisfies ListResponse<TestimonyResponse>);
}
async view(req: Request, res: Response) {
const parseParamsResult = testimonyParamsSchema.safeParse(req.params);
if (!parseParamsResult.success) {
return this.handleZodError(parseParamsResult.error, res, "params");
}
const params = parseParamsResult.data;
const testimony = await orm.em.findOne(Testimony, { id: params.id });
if (!testimony) {
return res.status(404).json({
data: null,
errors: [
{
path: "id",
location: "params",
message: "Testimony not found.",
},
],
} satisfies ErrorResponse);
}
return res.status(200).json({
data: this.mapper.mapEntityToResponse(testimony),
errors: null,
} satisfies SingleResponse<TestimonyResponse>);
}
async update(req: Request, res: Response) {
const parseParamsResult = testimonyParamsSchema.safeParse(req.params);
if (!parseParamsResult.success) {
return this.handleZodError(parseParamsResult.error, res, "params");
}
const params = parseParamsResult.data;
const parseBodyResult = testimonyRequestSchema.safeParse(req.body);
if (!parseBodyResult.success) {
return this.handleZodError(parseBodyResult.error, res, "body");
}
const body = parseBodyResult.data;
const testimony = await orm.em.findOne(Testimony, { id: params.id });
if (!testimony) {
return res.status(404).json({
data: null,
errors: [
{
path: "id",
location: "params",
message: "Testimony not found.",
},
],
} satisfies ErrorResponse);
}
testimony.review = body.review;
testimony.reviewer = body.reviewer;
testimony.rating = body.rating;
testimony.updatedAt = new Date();
await orm.em.flush();
return res.status(200).json({
data: this.mapper.mapEntityToResponse(testimony),
errors: null,
} satisfies SingleResponse<TestimonyResponse>);
}
async delete(req: Request, res: Response) {
const parseParamsResult = testimonyParamsSchema.safeParse(req.params);
if (!parseParamsResult.success) {
return this.handleZodError(parseParamsResult.error, res, "params");
}
const params = parseParamsResult.data;
const testimony = await orm.em.findOne(
Testimony,
{ id: params.id },
{
populate: ["*"],
},
);
if (!testimony) {
return res.status(404).json({
data: null,
errors: [
{
path: "id",
location: "params",
message: "Testimony not found.",
},
],
} satisfies ErrorResponse);
}
await orm.em.removeAndFlush(testimony);
return res.status(204).send();
}
public buildRouter(): Router {
const router = Router();
router.post(
"/",
createOrmContextMiddleware,
isAdminMiddleware(this.jwtService, [AdminPermission.createTestimony]),
this.create.bind(this),
);
router.get("/", createOrmContextMiddleware, this.list.bind(this));
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
router.put(
"/:id",
createOrmContextMiddleware,
isAdminMiddleware(this.jwtService, [AdminPermission.updateTestimony]),
this.update.bind(this),
);
router.delete(
"/:id",
createOrmContextMiddleware,
isAdminMiddleware(this.jwtService, [AdminPermission.deleteTestimony]),
this.delete.bind(this),
);
return router;
}
}

View File

@@ -0,0 +1,15 @@
import type { Testimony } from "@/database/entities/testimony.entity";
import type { TestimonyResponse } from "@/modules/testimony/testimony.types";
export class TestimonyMapper {
public mapEntityToResponse(testimony: Testimony): TestimonyResponse {
return {
id: testimony.id,
review: testimony.review,
reviewer: testimony.reviewer,
rating: testimony.rating,
created_at: testimony.createdAt,
updated_at: testimony.updatedAt,
};
}
}

View File

@@ -0,0 +1,24 @@
import z from "zod";
export const testimonyRequestSchema = z.object({
review: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(200, "Max 200 characters."),
reviewer: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(100, "Max 100 characters."),
rating: z
.number("Must be number.")
.int("Must be integer.")
.min(1, "Min 1.")
.max(5, "Max 5."),
});
export const testimonyParamsSchema = z.object({
id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(30, "Max 30 characters."),
});

View File

@@ -0,0 +1,18 @@
import type {
testimonyParamsSchema,
testimonyRequestSchema,
} from "@/modules/testimony/testimony.schemas";
import z from "zod";
export type TestimonyRequest = z.infer<typeof testimonyRequestSchema>;
export type TestimonyParams = z.infer<typeof testimonyParamsSchema>;
export type TestimonyResponse = {
id: string;
review: string;
reviewer: string;
rating: number;
created_at: Date;
updated_at: Date;
};