add testimony api
This commit is contained in:
@@ -38,6 +38,8 @@ import { PartnerMapper } from "@/modules/partner/partner.mapper";
|
|||||||
import { StaticController } from "@/modules/static/static.controller";
|
import { StaticController } from "@/modules/static/static.controller";
|
||||||
import { TagController } from "@/modules/tag/tag.controller";
|
import { TagController } from "@/modules/tag/tag.controller";
|
||||||
import { TagMapper } from "@/modules/tag/tag.mapper";
|
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 { TransportationClassController } from "@/modules/transportation-class/transportation-class.controller";
|
||||||
import { TransportationClassMapper } from "@/modules/transportation-class/transportation-class.mapper";
|
import { TransportationClassMapper } from "@/modules/transportation-class/transportation-class.mapper";
|
||||||
import { TransportationController } from "@/modules/transportation/transportation.controller";
|
import { TransportationController } from "@/modules/transportation/transportation.controller";
|
||||||
@@ -100,6 +102,7 @@ export class Application {
|
|||||||
);
|
);
|
||||||
const adminMapper = new AdminMapper();
|
const adminMapper = new AdminMapper();
|
||||||
const orderMapper = new OrderMapper(packageMapper, partnerMapper);
|
const orderMapper = new OrderMapper(packageMapper, partnerMapper);
|
||||||
|
const testimonyMapper = new TestimonyMapper();
|
||||||
const tagMapper = new TagMapper();
|
const tagMapper = new TagMapper();
|
||||||
const articleMapper = new ArticleMapper(tagMapper);
|
const articleMapper = new ArticleMapper(tagMapper);
|
||||||
|
|
||||||
@@ -170,6 +173,10 @@ export class Application {
|
|||||||
const whatsAppRouter = new WhatsAppController(
|
const whatsAppRouter = new WhatsAppController(
|
||||||
this._whatsAppService,
|
this._whatsAppService,
|
||||||
).buildRouter();
|
).buildRouter();
|
||||||
|
const testimonyRouter = new TestimonyController(
|
||||||
|
testimonyMapper,
|
||||||
|
this._jwtService,
|
||||||
|
).buildRouter();
|
||||||
const tagRouter = new TagController(
|
const tagRouter = new TagController(
|
||||||
tagMapper,
|
tagMapper,
|
||||||
this._jwtService,
|
this._jwtService,
|
||||||
@@ -196,6 +203,7 @@ export class Application {
|
|||||||
this._app.use("/orders", orderRouter);
|
this._app.use("/orders", orderRouter);
|
||||||
this._app.use("/statics", staticRouter);
|
this._app.use("/statics", staticRouter);
|
||||||
this._app.use("/whatsapp", whatsAppRouter);
|
this._app.use("/whatsapp", whatsAppRouter);
|
||||||
|
this._app.use("/testimonies", testimonyRouter);
|
||||||
this._app.use("/tags", tagRouter);
|
this._app.use("/tags", tagRouter);
|
||||||
this._app.use("/articles", articleRouter);
|
this._app.use("/articles", articleRouter);
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/database/entities/testimony.entity.ts
Normal file
29
src/database/entities/testimony.entity.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -55,6 +55,10 @@ export enum AdminPermission {
|
|||||||
createPartner = "partner:create",
|
createPartner = "partner:create",
|
||||||
updatePartner = "partner:update",
|
updatePartner = "partner:update",
|
||||||
deletePartner = "partner:delete",
|
deletePartner = "partner:delete",
|
||||||
|
// Testimony permissions
|
||||||
|
createTestimony = "testimony:create",
|
||||||
|
updateTestimony = "testimony:update",
|
||||||
|
deleteTestimony = "testimony:delete",
|
||||||
// Tag permissions
|
// Tag permissions
|
||||||
createTag = "tag:create",
|
createTag = "tag:create",
|
||||||
updateTag = "tag:update",
|
updateTag = "tag:update",
|
||||||
|
|||||||
210
src/modules/testimony/testimony.controller.ts
Normal file
210
src/modules/testimony/testimony.controller.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/modules/testimony/testimony.mapper.ts
Normal file
15
src/modules/testimony/testimony.mapper.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/modules/testimony/testimony.schemas.ts
Normal file
24
src/modules/testimony/testimony.schemas.ts
Normal 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."),
|
||||||
|
});
|
||||||
18
src/modules/testimony/testimony.types.ts
Normal file
18
src/modules/testimony/testimony.types.ts
Normal 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user