add core api
This commit is contained in:
207
src/modules/airline/airline.controller.ts
Normal file
207
src/modules/airline/airline.controller.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { Airline } from "@/database/entities/airline.entity";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { AirlineMapper } from "@/modules/airline/airline.mapper";
|
||||
import {
|
||||
airlineParamsSchema,
|
||||
airlineRequestSchema,
|
||||
} from "@/modules/airline/airline.schemas";
|
||||
import type { AirlineResponse } from "@/modules/airline/airline.types";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class AirlineController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: AirlineMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = airlineRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const logoFile = await this.fileStorage.storeFile(
|
||||
Buffer.from(body.logo, "base64"),
|
||||
);
|
||||
|
||||
const airline = orm.em.create(Airline, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
code: body.code,
|
||||
logo: logoFile.name,
|
||||
skytraxRating: body.skytrax_rating,
|
||||
skytraxType: this.mapper.mapSkytraxType(body.skytrax_type),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: this.mapper.mapEntityToResponse(airline),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<AirlineResponse>);
|
||||
}
|
||||
|
||||
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(Airline);
|
||||
|
||||
const airlines = await orm.em.find(
|
||||
Airline,
|
||||
{},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: airlines.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<AirlineResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = airlineParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const airline = await orm.em.findOne(Airline, { id: params.id });
|
||||
if (!airline) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Airline not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(airline),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<AirlineResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = airlineParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = airlineRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const airline = await orm.em.findOne(Airline, { id: params.id });
|
||||
if (!airline) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Airline not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await this.fileStorage.storeFile(
|
||||
Buffer.from(body.logo, "base64"),
|
||||
airline.logo,
|
||||
);
|
||||
|
||||
airline.name = body.name;
|
||||
airline.code = body.code;
|
||||
airline.skytraxRating = body.skytrax_rating;
|
||||
airline.skytraxType = this.mapper.mapSkytraxType(body.skytrax_type);
|
||||
airline.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(airline),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<AirlineResponse>);
|
||||
}
|
||||
|
||||
async delete(req: Request, res: Response) {
|
||||
const parseParamsResult = airlineParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const airline = await orm.em.findOne(
|
||||
Airline,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!airline) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Airline not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await this.fileStorage.removeFile(airline.logo);
|
||||
|
||||
await orm.em.removeAndFlush(airline);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
34
src/modules/airline/airline.mapper.ts
Normal file
34
src/modules/airline/airline.mapper.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Airline } from "@/database/entities/airline.entity";
|
||||
import { SkytraxType } from "@/database/enums/skytrax-type.enum";
|
||||
import type {
|
||||
AirlineRequest,
|
||||
AirlineResponse,
|
||||
} from "@/modules/airline/airline.types";
|
||||
|
||||
export class AirlineMapper {
|
||||
public constructor() {}
|
||||
|
||||
public mapSkytraxType(
|
||||
skytraxType: AirlineRequest["skytrax_type"],
|
||||
): SkytraxType {
|
||||
switch (skytraxType) {
|
||||
case "full_service":
|
||||
return SkytraxType.fullService;
|
||||
case "low_cost":
|
||||
return SkytraxType.lowCost;
|
||||
}
|
||||
}
|
||||
|
||||
public mapEntityToResponse(airline: Airline): AirlineResponse {
|
||||
return {
|
||||
id: airline.id,
|
||||
name: airline.name,
|
||||
code: airline.code,
|
||||
logo: airline.logo,
|
||||
skytrax_rating: airline.skytraxRating,
|
||||
skytrax_type: airline.skytraxType,
|
||||
created_at: airline.createdAt,
|
||||
updated_at: airline.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ export const airlineRequestSchema = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
export const airlineQuerySchema = z.object({
|
||||
slug: z
|
||||
export const airlineParamsSchema = z.object({
|
||||
id: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import type {
|
||||
airlineQuerySchema,
|
||||
airlineParamsSchema,
|
||||
airlineRequestSchema,
|
||||
} from "@/modules/airline/airline.schemas";
|
||||
import z from "zod";
|
||||
|
||||
export type AirlineRequest = z.infer<typeof airlineRequestSchema>;
|
||||
|
||||
export type AirlineQuery = z.infer<typeof airlineQuerySchema>;
|
||||
export type AirlineParams = z.infer<typeof airlineParamsSchema>;
|
||||
|
||||
export type AirlineResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
code: string;
|
||||
logo: string;
|
||||
|
||||
235
src/modules/airport/airport.controller.ts
Normal file
235
src/modules/airport/airport.controller.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { Airport } from "@/database/entities/airport.entity";
|
||||
import { City } from "@/database/entities/city.entity";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { AirportMapper } from "@/modules/airport/airport.mapper";
|
||||
import {
|
||||
airportParamsSchema,
|
||||
airportRequestSchema,
|
||||
} from "@/modules/airport/airport.schemas";
|
||||
import type { AirportResponse } from "@/modules/airport/airport.types";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class AirportController extends Controller {
|
||||
public constructor(private readonly mapper: AirportMapper) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = airportRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const city = await orm.em.findOne(
|
||||
City,
|
||||
{ id: body.city_id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!city) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "city_id",
|
||||
location: "body",
|
||||
message: "City not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const airport = orm.em.create(Airport, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
code: body.code,
|
||||
city: city,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: this.mapper.mapEntityToResponse(airport),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<AirportResponse>);
|
||||
}
|
||||
|
||||
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(Airport);
|
||||
|
||||
const airports = await orm.em.find(
|
||||
Airport,
|
||||
{},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: airports.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<AirportResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = airportParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const airport = await orm.em.findOne(
|
||||
Airport,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!airport) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Airport not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(airport),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<AirportResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = airportParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = airportRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const city = await orm.em.findOne(
|
||||
City,
|
||||
{ id: body.city_id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!city) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "city_id",
|
||||
location: "body",
|
||||
message: "City not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const airport = await orm.em.findOne(
|
||||
Airport,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!airport) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Airport not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
airport.name = body.name;
|
||||
airport.code = body.code;
|
||||
airport.city = city;
|
||||
airport.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(airport),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<AirportResponse>);
|
||||
}
|
||||
|
||||
async delete(req: Request, res: Response) {
|
||||
const parseParamsResult = airportParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const airport = await orm.em.findOne(
|
||||
Airport,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!airport) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Airport not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(airport);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
18
src/modules/airport/airport.mapper.ts
Normal file
18
src/modules/airport/airport.mapper.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Airport } from "@/database/entities/airport.entity";
|
||||
import type { AirportResponse } from "@/modules/airport/airport.types";
|
||||
import type { CityMapper } from "@/modules/city/city.mapper";
|
||||
|
||||
export class AirportMapper {
|
||||
public constructor(private readonly cityMapper: CityMapper) {}
|
||||
|
||||
public mapEntityToResponse(airport: Airport): AirportResponse {
|
||||
return {
|
||||
id: airport.id,
|
||||
name: airport.name,
|
||||
code: airport.code,
|
||||
city: this.cityMapper.mapEntityToResponse(airport.city),
|
||||
created_at: airport.createdAt,
|
||||
updated_at: airport.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,15 @@ export const airportRequestSchema = z.object({
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(10, "Max 10 characters."),
|
||||
city_slug: z
|
||||
.string("Must be string.")
|
||||
city_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
|
||||
export const airportQuerySchema = z.object({
|
||||
slug: z
|
||||
.string("Must be string.")
|
||||
export const airportParamsSchema = z.object({
|
||||
id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
airportQuerySchema,
|
||||
airportParamsSchema,
|
||||
airportRequestSchema,
|
||||
} from "@/modules/airport/airport.schemas";
|
||||
import type { CityResponse } from "@/modules/city/city.types";
|
||||
@@ -7,11 +7,10 @@ import z from "zod";
|
||||
|
||||
export type AirportRequest = z.infer<typeof airportRequestSchema>;
|
||||
|
||||
export type AirportQuery = z.infer<typeof airportQuerySchema>;
|
||||
export type AirportParams = z.infer<typeof airportParamsSchema>;
|
||||
|
||||
export type AirportResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
code: string;
|
||||
city: CityResponse;
|
||||
|
||||
221
src/modules/city/city.controller.ts
Normal file
221
src/modules/city/city.controller.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { City } from "@/database/entities/city.entity";
|
||||
import { Country } from "@/database/entities/country.entity";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { CityMapper } from "@/modules/city/city.mapper";
|
||||
import {
|
||||
cityParamsSchema,
|
||||
cityRequestSchema,
|
||||
} from "@/modules/city/city.schemas";
|
||||
import type { CityResponse } from "@/modules/city/city.types";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class CityController extends Controller {
|
||||
public constructor(private readonly mapper: CityMapper) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = cityRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const country = await orm.em.findOne(Country, { id: body.country_id });
|
||||
if (!country) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "country_id",
|
||||
location: "body",
|
||||
message: "Country not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const city = orm.em.create(City, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
country,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: this.mapper.mapEntityToResponse(city),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<CityResponse>);
|
||||
}
|
||||
|
||||
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(City);
|
||||
|
||||
const cities = await orm.em.find(
|
||||
City,
|
||||
{},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: cities.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<CityResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = cityParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const city = await orm.em.findOne(
|
||||
City,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!city) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "City not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(city),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<CityResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = cityParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = cityRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const country = await orm.em.findOne(Country, { id: body.country_id });
|
||||
if (!country) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "country_id",
|
||||
location: "body",
|
||||
message: "Country not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const city = await orm.em.findOne(City, { id: params.id });
|
||||
if (!city) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "City not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
city.name = body.name;
|
||||
city.country = country;
|
||||
city.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(city),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<CityResponse>);
|
||||
}
|
||||
|
||||
async delete(req: Request, res: Response) {
|
||||
const parseParamsResult = cityParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const city = await orm.em.findOne(
|
||||
City,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!city) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "City not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(city);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
17
src/modules/city/city.mapper.ts
Normal file
17
src/modules/city/city.mapper.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { City } from "@/database/entities/city.entity";
|
||||
import type { CityResponse } from "@/modules/city/city.types";
|
||||
import type { CountryMapper } from "@/modules/country/country.mapper";
|
||||
|
||||
export class CityMapper {
|
||||
public constructor(private readonly countryMapper: CountryMapper) {}
|
||||
|
||||
public mapEntityToResponse(city: City): CityResponse {
|
||||
return {
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
country: this.countryMapper.mapEntityToResponse(city.country),
|
||||
created_at: city.createdAt,
|
||||
updated_at: city.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,14 @@ export const cityRequestSchema = z.object({
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
country_slug: z
|
||||
.string("Must be string.")
|
||||
country_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
|
||||
export const cityQuerySchema = z.object({
|
||||
slug: z
|
||||
export const cityParamsSchema = z.object({
|
||||
id: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
cityQuerySchema,
|
||||
cityParamsSchema,
|
||||
cityRequestSchema,
|
||||
} from "@/modules/city/city.schemas";
|
||||
import type { CountryResponse } from "@/modules/country/country.types";
|
||||
@@ -7,11 +7,10 @@ import z from "zod";
|
||||
|
||||
export type CityRequest = z.infer<typeof cityRequestSchema>;
|
||||
|
||||
export type CityQuery = z.infer<typeof cityQuerySchema>;
|
||||
export type CityParams = z.infer<typeof cityParamsSchema>;
|
||||
|
||||
export type CityResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
country: CountryResponse;
|
||||
created_at: Date;
|
||||
|
||||
185
src/modules/country/country.controller.ts
Normal file
185
src/modules/country/country.controller.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { Country } from "@/database/entities/country.entity";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { CountryMapper } from "@/modules/country/country.mapper";
|
||||
import {
|
||||
countryParamsSchema,
|
||||
countryRequestSchema,
|
||||
} from "@/modules/country/country.schemas";
|
||||
import type { CountryResponse } from "@/modules/country/country.types";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class CountryController extends Controller {
|
||||
public constructor(private readonly mapper: CountryMapper) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = countryRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const country = orm.em.create(Country, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: this.mapper.mapEntityToResponse(country),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<CountryResponse>);
|
||||
}
|
||||
|
||||
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(Country);
|
||||
|
||||
const countries = await orm.em.find(
|
||||
Country,
|
||||
{},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: countries.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<CountryResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = countryParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const country = await orm.em.findOne(Country, { id: params.id });
|
||||
if (!country) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Country not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(country),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<CountryResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = countryParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = countryRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const country = await orm.em.findOne(Country, { id: params.id });
|
||||
if (!country) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Country not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
country.name = body.name;
|
||||
country.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(country),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<CountryResponse>);
|
||||
}
|
||||
|
||||
async delete(req: Request, res: Response) {
|
||||
const parseParamsResult = countryParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const country = await orm.em.findOne(
|
||||
Country,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!country) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Country not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(country);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
13
src/modules/country/country.mapper.ts
Normal file
13
src/modules/country/country.mapper.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Country } from "@/database/entities/country.entity";
|
||||
import type { CountryResponse } from "@/modules/country/country.types";
|
||||
|
||||
export class CountryMapper {
|
||||
public mapEntityToResponse(country: Country): CountryResponse {
|
||||
return {
|
||||
id: country.id,
|
||||
name: country.name,
|
||||
created_at: country.createdAt,
|
||||
updated_at: country.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ export const countryRequestSchema = z.object({
|
||||
.max(100, "Max 100 characters."),
|
||||
});
|
||||
|
||||
export const countryQuerySchema = z.object({
|
||||
slug: z
|
||||
.string("Must be string.")
|
||||
export const countryParamsSchema = z.object({
|
||||
id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import type {
|
||||
countryQuerySchema,
|
||||
countryParamsSchema,
|
||||
countryRequestSchema,
|
||||
} from "@/modules/country/country.schemas";
|
||||
import z from "zod";
|
||||
|
||||
export type CountryRequest = z.infer<typeof countryRequestSchema>;
|
||||
|
||||
export type CountryQuery = z.infer<typeof countryQuerySchema>;
|
||||
export type CountryParams = z.infer<typeof countryParamsSchema>;
|
||||
|
||||
export type CountryResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
|
||||
650
src/modules/flight/flight.controller.ts
Normal file
650
src/modules/flight/flight.controller.ts
Normal file
@@ -0,0 +1,650 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { Airline } from "@/database/entities/airline.entity";
|
||||
import { Airport } from "@/database/entities/airport.entity";
|
||||
import { FlightClass } from "@/database/entities/flight-class.entity";
|
||||
import { Flight } from "@/database/entities/flight.entity";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { FlightMapper } from "@/modules/flight/flight.mapper";
|
||||
import {
|
||||
flightClassParamsSchema,
|
||||
flightClassRequestSchema,
|
||||
flightParamsSchema,
|
||||
flightRequestSchema,
|
||||
} from "@/modules/flight/flight.schemas";
|
||||
import type {
|
||||
FlightClassResponse,
|
||||
FlightResponse,
|
||||
} from "@/modules/flight/flight.types";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class FlightController extends Controller {
|
||||
public constructor(private readonly mapper: FlightMapper) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = flightRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const airline = await orm.em.findOne(
|
||||
Airline,
|
||||
{
|
||||
id: body.airline_id,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!airline) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "airline_id",
|
||||
location: "body",
|
||||
message: "Airline not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const departureAirport = await orm.em.findOne(
|
||||
Airport,
|
||||
{
|
||||
id: body.departure_airport_id,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!departureAirport) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "departure_airport_id",
|
||||
location: "body",
|
||||
message: "Airport not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const arrivalAirport = await orm.em.findOne(
|
||||
Airport,
|
||||
{
|
||||
id: body.arrival_airport_id,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!arrivalAirport) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "arrival_airport_id",
|
||||
location: "body",
|
||||
message: "Airport not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const flight = orm.em.create(Flight, {
|
||||
id: ulid(),
|
||||
airline,
|
||||
number: body.number,
|
||||
departureAirport,
|
||||
departureTerminal: body.departure_terminal,
|
||||
departureGate: body.departure_gate,
|
||||
departureTime: `${body.departure_time.getHours()}:${body.departure_time.getMinutes()}:00`,
|
||||
arrivalAirport,
|
||||
arrivalTerminal: body.arrival_terminal,
|
||||
arrivalGate: body.arrival_gate,
|
||||
duration: body.duration,
|
||||
aircraft: body.aircraft,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: this.mapper.mapEntityToResponse(flight),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<FlightResponse>);
|
||||
}
|
||||
|
||||
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(Flight);
|
||||
|
||||
const flights = await orm.em.find(
|
||||
Flight,
|
||||
{},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: flights.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<FlightResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = flightParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const flight = await orm.em.findOne(
|
||||
Flight,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!flight) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Flight not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(flight),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<FlightResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = flightParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = flightRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const airline = await orm.em.findOne(
|
||||
Airline,
|
||||
{
|
||||
id: body.airline_id,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!airline) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "airline_id",
|
||||
location: "body",
|
||||
message: "Airline not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const departureAirport = await orm.em.findOne(
|
||||
Airport,
|
||||
{
|
||||
id: body.departure_airport_id,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!departureAirport) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "departure_airport_id",
|
||||
location: "body",
|
||||
message: "Airport not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const arrivalAirport = await orm.em.findOne(
|
||||
Airport,
|
||||
{
|
||||
id: body.arrival_airport_id,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!arrivalAirport) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "arrival_airport_id",
|
||||
location: "body",
|
||||
message: "Airport not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const flight = await orm.em.findOne(
|
||||
Flight,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!flight) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Flight not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
flight.airline = airline;
|
||||
flight.number = body.number;
|
||||
flight.departureAirport = departureAirport;
|
||||
flight.departureTerminal = body.departure_terminal;
|
||||
flight.departureGate = body.departure_gate;
|
||||
flight.departureTime = `${body.departure_time.getHours()}:${body.departure_time.getMinutes()}:00`;
|
||||
flight.arrivalAirport = arrivalAirport;
|
||||
flight.arrivalTerminal = body.arrival_terminal;
|
||||
flight.arrivalGate = body.arrival_gate;
|
||||
flight.duration = body.duration;
|
||||
flight.aircraft = body.aircraft;
|
||||
flight.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(flight),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<FlightResponse>);
|
||||
}
|
||||
|
||||
async delete(req: Request, res: Response) {
|
||||
const parseParamsResult = flightParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const flight = await orm.em.findOne(
|
||||
Flight,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!flight) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Flight not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(flight);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
async createClass(req: Request, res: Response) {
|
||||
const parseParamsResult = flightParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = flightClassRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const flight = await orm.em.findOne(
|
||||
Flight,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!flight) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Flight not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const flightClass = orm.em.create(FlightClass, {
|
||||
id: ulid(),
|
||||
flight,
|
||||
class: body.class,
|
||||
seatLayout: body.seat_layout,
|
||||
baggage: body.baggage,
|
||||
cabinBaggage: body.cabin_baggage,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: this.mapper.mapClassEntityToResponse(flightClass),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<FlightClassResponse>);
|
||||
}
|
||||
|
||||
async listClasses(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 parseParamsResult = flightParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const flight = await orm.em.findOne(
|
||||
Flight,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!flight) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Flight not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const count = await orm.em.count(FlightClass, {
|
||||
flight: flight,
|
||||
});
|
||||
|
||||
const classes = await orm.em.find(
|
||||
FlightClass,
|
||||
{ flight: flight },
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: classes.map(this.mapper.mapClassEntityToResponse.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<FlightClassResponse>);
|
||||
}
|
||||
|
||||
async viewClass(req: Request, res: Response) {
|
||||
const parseParamsResult = flightClassParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const flight = await orm.em.findOne(
|
||||
Flight,
|
||||
{ id: params.flight_id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!flight) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "flight_id",
|
||||
location: "params",
|
||||
message: "Flight not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const flightClass = await orm.em.findOne(
|
||||
FlightClass,
|
||||
{
|
||||
id: params.id,
|
||||
flight: flight,
|
||||
},
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!flightClass) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Flight class not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapClassEntityToResponse(flightClass),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<FlightClassResponse>);
|
||||
}
|
||||
|
||||
async updateClass(req: Request, res: Response) {
|
||||
const parseParamsResult = flightClassParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = flightClassRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const flight = await orm.em.findOne(
|
||||
Flight,
|
||||
{ id: params.flight_id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!flight) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "flight_id",
|
||||
location: "params",
|
||||
message: "Flight not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const flightClass = await orm.em.findOne(
|
||||
FlightClass,
|
||||
{
|
||||
id: params.id,
|
||||
flight: flight,
|
||||
},
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!flightClass) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Flight class not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
flightClass.class = body.class;
|
||||
flightClass.seatLayout = body.seat_layout;
|
||||
flightClass.baggage = body.baggage;
|
||||
flightClass.cabinBaggage = body.cabin_baggage;
|
||||
flightClass.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapClassEntityToResponse(flightClass),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<FlightClassResponse>);
|
||||
}
|
||||
|
||||
async deleteClass(req: Request, res: Response) {
|
||||
const parseParamsResult = flightClassParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const flight = await orm.em.findOne(
|
||||
Flight,
|
||||
{ id: params.flight_id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!flight) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "flight_id",
|
||||
location: "params",
|
||||
message: "Flight not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const flightClass = await orm.em.findOne(
|
||||
FlightClass,
|
||||
{
|
||||
id: params.id,
|
||||
flight: flight,
|
||||
},
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!flightClass) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Flight class not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(flightClass);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post("/:id/classes", ormMiddleware, this.createClass.bind(this));
|
||||
router.get("/:id/classes", ormMiddleware, this.listClasses.bind(this));
|
||||
router.get(
|
||||
"/:flight_id/classes/:id",
|
||||
ormMiddleware,
|
||||
this.viewClass.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:flight_id/classes/:id",
|
||||
ormMiddleware,
|
||||
this.updateClass.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:flight_id/classes/:id",
|
||||
ormMiddleware,
|
||||
this.deleteClass.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
62
src/modules/flight/flight.mapper.ts
Normal file
62
src/modules/flight/flight.mapper.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { FlightClass } from "@/database/entities/flight-class.entity";
|
||||
import type { Flight } from "@/database/entities/flight.entity";
|
||||
import type { AirlineMapper } from "@/modules/airline/airline.mapper";
|
||||
import type { AirportMapper } from "@/modules/airport/airport.mapper";
|
||||
import type {
|
||||
FlightClassResponse,
|
||||
FlightResponse,
|
||||
} from "@/modules/flight/flight.types";
|
||||
import * as dateFns from "date-fns";
|
||||
|
||||
export class FlightMapper {
|
||||
public constructor(
|
||||
private readonly airlineMapper: AirlineMapper,
|
||||
private readonly airportMapper: AirportMapper,
|
||||
) {}
|
||||
|
||||
public mapEntityToResponse(flight: Flight): FlightResponse {
|
||||
const departureTime = dateFns.parse(
|
||||
flight.departureTime,
|
||||
"H:mm:ss",
|
||||
new Date(),
|
||||
);
|
||||
const arrivalTime = dateFns.addMinutes(departureTime, flight.duration);
|
||||
|
||||
return {
|
||||
id: flight.id,
|
||||
airline: this.airlineMapper.mapEntityToResponse(flight.airline),
|
||||
number: flight.number,
|
||||
departure_airport: this.airportMapper.mapEntityToResponse(
|
||||
flight.departureAirport,
|
||||
),
|
||||
departure_terminal: flight.departureTerminal,
|
||||
departure_gate: flight.departureGate,
|
||||
departure_time: dateFns.format(departureTime, "HH:mm"),
|
||||
arrival_airport: this.airportMapper.mapEntityToResponse(
|
||||
flight.arrivalAirport,
|
||||
),
|
||||
arrival_terminal: flight.arrivalTerminal,
|
||||
arrival_gate: flight.arrivalGate,
|
||||
arrival_time: dateFns.format(arrivalTime, "HH:mm"),
|
||||
duration: flight.duration,
|
||||
aircraft: flight.aircraft,
|
||||
created_at: flight.createdAt,
|
||||
updated_at: flight.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
public mapClassEntityToResponse(
|
||||
flightClass: FlightClass,
|
||||
): FlightClassResponse {
|
||||
return {
|
||||
id: flightClass.id,
|
||||
flight: this.mapEntityToResponse(flightClass.flight),
|
||||
class: flightClass.class,
|
||||
seat_layout: flightClass.seatLayout,
|
||||
baggage: flightClass.baggage,
|
||||
cabin_baggage: flightClass.cabinBaggage,
|
||||
created_at: flightClass.createdAt,
|
||||
updated_at: flightClass.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,18 @@ import { timeSchema } from "@/common/schemas";
|
||||
import z from "zod";
|
||||
|
||||
export const flightRequestSchema = z.object({
|
||||
airline_slug: z
|
||||
.string("Must be string.")
|
||||
airline_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
number: z
|
||||
.number("Must be number.")
|
||||
.int("Must be integer.")
|
||||
.positive("Must be positive."),
|
||||
departure_airport_slug: z
|
||||
.string("Must be string.")
|
||||
departure_airport_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
departure_terminal: z
|
||||
.string("Must be string.")
|
||||
.max(100, "Max 100 characters.")
|
||||
@@ -23,10 +23,10 @@ export const flightRequestSchema = z.object({
|
||||
.max(100, "Max 100 characters.")
|
||||
.nullable(),
|
||||
departure_time: timeSchema,
|
||||
arrival_airport_slug: z
|
||||
.string("Must be string.")
|
||||
arrival_airport_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
arrival_terminal: z
|
||||
.string("Must be string.")
|
||||
.max(100, "Max 100 characters.")
|
||||
@@ -64,20 +64,20 @@ export const flightClassRequestSchema = z.object({
|
||||
.positive("Must be positive."),
|
||||
});
|
||||
|
||||
export const flightQuerySchema = z.object({
|
||||
slug: z
|
||||
.string("Must be string.")
|
||||
export const flightParamsSchema = z.object({
|
||||
id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(220, "Max 220 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
|
||||
export const flightClassQuerySchema = z.object({
|
||||
flight_slug: z
|
||||
.string("Must be string.")
|
||||
export const flightClassParamsSchema = z.object({
|
||||
flight_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(220, "Max 220 characters."),
|
||||
slug: z
|
||||
.string("Must be string.")
|
||||
.max(30, "Max 30 characters."),
|
||||
id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(420, "Max 420 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { AirlineResponse } from "@/modules/airline/airline.types";
|
||||
import type { AirportResponse } from "@/modules/airport/airport.types";
|
||||
import type {
|
||||
flightClassQuerySchema,
|
||||
flightClassParamsSchema,
|
||||
flightClassRequestSchema,
|
||||
flightQuerySchema,
|
||||
flightParamsSchema,
|
||||
flightRequestSchema,
|
||||
} from "@/modules/flight/flight.schemas";
|
||||
import z from "zod";
|
||||
@@ -12,13 +12,12 @@ export type FlightRequest = z.infer<typeof flightRequestSchema>;
|
||||
|
||||
export type FlightClassRequest = z.infer<typeof flightClassRequestSchema>;
|
||||
|
||||
export type FlightQuery = z.infer<typeof flightQuerySchema>;
|
||||
export type FlightQuery = z.infer<typeof flightParamsSchema>;
|
||||
|
||||
export type FlightClassQuery = z.infer<typeof flightClassQuerySchema>;
|
||||
export type FlightClassQuery = z.infer<typeof flightClassParamsSchema>;
|
||||
|
||||
export type FlightResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
airline: AirlineResponse;
|
||||
number: number;
|
||||
departure_airport: AirportResponse;
|
||||
@@ -37,7 +36,6 @@ export type FlightResponse = {
|
||||
|
||||
export type FlightClassResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
flight: FlightResponse;
|
||||
class: string;
|
||||
seat_layout: string;
|
||||
|
||||
195
src/modules/hotel-facility/hotel-facility.controller.ts
Normal file
195
src/modules/hotel-facility/hotel-facility.controller.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { HotelFacility } from "@/database/entities/hotel-facility.entity";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { HotelFacilityMapper } from "@/modules/hotel-facility/hotel-facility.mapper";
|
||||
import {
|
||||
hotelFacilityParamsSchema,
|
||||
hotelFacilityRequestSchema,
|
||||
} from "@/modules/hotel-facility/hotel-facility.schemas";
|
||||
import type { HotelFacilityResponse } from "@/modules/hotel-facility/hotel-facility.types";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class HotelFacilityController extends Controller {
|
||||
public constructor(private readonly mapper: HotelFacilityMapper) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = hotelFacilityRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const hotelFacility = orm.em.create(HotelFacility, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
icon: body.icon,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: this.mapper.mapEntityToResponse(hotelFacility),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<HotelFacilityResponse>);
|
||||
}
|
||||
|
||||
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(HotelFacility);
|
||||
|
||||
const hotelFacilities = await orm.em.find(
|
||||
HotelFacility,
|
||||
{},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: hotelFacilities.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<HotelFacilityResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = hotelFacilityParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const hotelFacility = await orm.em.findOne(HotelFacility, {
|
||||
id: params.id,
|
||||
});
|
||||
if (!hotelFacility) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Hotel facility not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(hotelFacility),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<HotelFacilityResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = hotelFacilityParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = hotelFacilityRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const hotelFacility = await orm.em.findOne(HotelFacility, {
|
||||
id: params.id,
|
||||
});
|
||||
if (!hotelFacility) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Hotel facility not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
hotelFacility.name = body.name;
|
||||
hotelFacility.icon = body.icon;
|
||||
hotelFacility.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(hotelFacility),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<HotelFacilityResponse>);
|
||||
}
|
||||
|
||||
async delete(req: Request, res: Response) {
|
||||
const parseParamsResult = hotelFacilityParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const hotelFacility = await orm.em.findOne(
|
||||
HotelFacility,
|
||||
{
|
||||
id: params.id,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!hotelFacility) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Hotel facility not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(hotelFacility);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
16
src/modules/hotel-facility/hotel-facility.mapper.ts
Normal file
16
src/modules/hotel-facility/hotel-facility.mapper.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { HotelFacility } from "@/database/entities/hotel-facility.entity";
|
||||
import type { HotelFacilityResponse } from "@/modules/hotel-facility/hotel-facility.types";
|
||||
|
||||
export class HotelFacilityMapper {
|
||||
public mapEntityToResponse(
|
||||
hotelFacility: HotelFacility,
|
||||
): HotelFacilityResponse {
|
||||
return {
|
||||
id: hotelFacility.id,
|
||||
name: hotelFacility.name,
|
||||
icon: hotelFacility.icon,
|
||||
created_at: hotelFacility.createdAt,
|
||||
updated_at: hotelFacility.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
19
src/modules/hotel-facility/hotel-facility.schemas.ts
Normal file
19
src/modules/hotel-facility/hotel-facility.schemas.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import z from "zod";
|
||||
|
||||
export const hotelFacilityRequestSchema = z.object({
|
||||
name: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
icon: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
});
|
||||
|
||||
export const hotelFacilityParamsSchema = z.object({
|
||||
id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
17
src/modules/hotel-facility/hotel-facility.types.ts
Normal file
17
src/modules/hotel-facility/hotel-facility.types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type {
|
||||
hotelFacilityParamsSchema,
|
||||
hotelFacilityRequestSchema,
|
||||
} from "@/modules/hotel-facility/hotel-facility.schemas";
|
||||
import z from "zod";
|
||||
|
||||
export type HotelFacilityRequest = z.infer<typeof hotelFacilityRequestSchema>;
|
||||
|
||||
export type HotelFacilityParams = z.infer<typeof hotelFacilityParamsSchema>;
|
||||
|
||||
export type HotelFacilityResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
343
src/modules/hotel/hotel.controller.ts
Normal file
343
src/modules/hotel/hotel.controller.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { City } from "@/database/entities/city.entity";
|
||||
import { HotelFacility } from "@/database/entities/hotel-facility.entity";
|
||||
import { HotelImage } from "@/database/entities/hotel-image.entity";
|
||||
import { Hotel } from "@/database/entities/hotel.entity";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { HotelMapper } from "@/modules/hotel/hotel.mapper";
|
||||
import {
|
||||
hotelParamsSchema,
|
||||
hotelRequestSchema,
|
||||
} from "@/modules/hotel/hotel.schemas";
|
||||
import type { HotelResponse } from "@/modules/hotel/hotel.types";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class HotelController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: HotelMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = hotelRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const city = await orm.em.findOne(
|
||||
City,
|
||||
{ id: body.city_id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!city) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "city_id",
|
||||
location: "body",
|
||||
message: "City not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const hotel = orm.em.create(Hotel, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
city: city,
|
||||
star: body.star,
|
||||
googleMapsLink: body.google_maps_link,
|
||||
googleMapsEmbed: body.google_maps_embed,
|
||||
googleReviewsLink: body.google_reviews_link,
|
||||
description: body.description,
|
||||
address: body.address,
|
||||
landmark: body.landmark,
|
||||
distanceToLandmark: body.distance_to_landmark,
|
||||
foodType: body.food_type,
|
||||
foodAmount: body.food_amount,
|
||||
foodMenu: body.food_menu,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
for (const [index, image] of body.images.entries()) {
|
||||
const imageFile = await this.fileStorage.storeFile(
|
||||
Buffer.from(image, "base64"),
|
||||
);
|
||||
|
||||
const hotelImage = orm.em.create(HotelImage, {
|
||||
id: ulid(),
|
||||
hotel: hotel,
|
||||
src: imageFile.name,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
hotel.images.add(hotelImage);
|
||||
}
|
||||
|
||||
for (const [index, facilityId] of body.facility_ids.entries()) {
|
||||
const facility = await orm.em.findOne(HotelFacility, {
|
||||
id: facilityId,
|
||||
});
|
||||
if (!facility) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: `facility_ids.${index}`,
|
||||
location: "body",
|
||||
message: "Hotel facility not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
hotel.facilities.add(facility);
|
||||
}
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: this.mapper.mapEntityToResponse(hotel),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<HotelResponse>);
|
||||
}
|
||||
|
||||
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(Hotel);
|
||||
|
||||
const hotels = await orm.em.find(
|
||||
Hotel,
|
||||
{},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: hotels.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<HotelResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = hotelParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const hotel = await orm.em.findOne(
|
||||
Hotel,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!hotel) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Hotel not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(hotel),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<HotelResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = hotelParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = hotelRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const city = await orm.em.findOne(
|
||||
City,
|
||||
{ id: body.city_id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!city) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "city_id",
|
||||
location: "body",
|
||||
message: "City not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const hotel = await orm.em.findOne(
|
||||
Hotel,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!hotel) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Hotel not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
hotel.name = body.name;
|
||||
hotel.city = city;
|
||||
hotel.star = body.star;
|
||||
hotel.googleMapsLink = body.google_maps_link;
|
||||
hotel.googleMapsEmbed = body.google_maps_embed;
|
||||
hotel.googleReviewsLink = body.google_reviews_link;
|
||||
hotel.description = body.description;
|
||||
hotel.address = body.address;
|
||||
hotel.landmark = body.landmark;
|
||||
hotel.distanceToLandmark = body.distance_to_landmark;
|
||||
hotel.foodType = body.food_type;
|
||||
hotel.foodAmount = body.food_amount;
|
||||
hotel.foodMenu = body.food_menu;
|
||||
hotel.updatedAt = new Date();
|
||||
|
||||
for (const hotelImage of hotel.images) {
|
||||
await this.fileStorage.removeFile(hotelImage.src);
|
||||
|
||||
orm.em.remove(hotelImage);
|
||||
}
|
||||
for (const [index, image] of body.images.entries()) {
|
||||
const imageFile = await this.fileStorage.storeFile(
|
||||
Buffer.from(image, "base64"),
|
||||
);
|
||||
|
||||
const hotelImage = orm.em.create(HotelImage, {
|
||||
id: ulid(),
|
||||
hotel: hotel,
|
||||
src: imageFile.name,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
hotel.images.add(hotelImage);
|
||||
}
|
||||
|
||||
hotel.facilities.removeAll();
|
||||
for (const [index, facilityId] of body.facility_ids.entries()) {
|
||||
const facility = await orm.em.findOne(HotelFacility, {
|
||||
id: facilityId,
|
||||
});
|
||||
if (!facility) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: `facility_ids.${index}`,
|
||||
location: "body",
|
||||
message: "Hotel facility not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
hotel.facilities.add(facility);
|
||||
}
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(hotel),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<HotelResponse>);
|
||||
}
|
||||
|
||||
async delete(req: Request, res: Response) {
|
||||
const parseParamsResult = hotelParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const hotel = await orm.em.findOne(
|
||||
Hotel,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!hotel) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Hotel not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
for (const image of hotel.images.getItems()) {
|
||||
await this.fileStorage.removeFile(image.src);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(hotel);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
40
src/modules/hotel/hotel.mapper.ts
Normal file
40
src/modules/hotel/hotel.mapper.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Hotel } from "@/database/entities/hotel.entity";
|
||||
import type { CityMapper } from "@/modules/city/city.mapper";
|
||||
import type { HotelFacilityMapper } from "@/modules/hotel-facility/hotel-facility.mapper";
|
||||
import type { HotelResponse } from "@/modules/hotel/hotel.types";
|
||||
|
||||
export class HotelMapper {
|
||||
public constructor(
|
||||
private readonly cityMapper: CityMapper,
|
||||
private readonly hotelFacilityMapper: HotelFacilityMapper,
|
||||
) {}
|
||||
|
||||
public mapEntityToResponse(hotel: Hotel): HotelResponse {
|
||||
return {
|
||||
id: hotel.id,
|
||||
name: hotel.name,
|
||||
city: this.cityMapper.mapEntityToResponse(hotel.city),
|
||||
star: hotel.star,
|
||||
images: hotel.images.getItems().map((image) => image.src),
|
||||
google_maps_link: hotel.googleMapsLink,
|
||||
google_maps_embed: hotel.googleMapsEmbed,
|
||||
google_reviews_link: hotel.googleReviewsLink,
|
||||
description: hotel.description,
|
||||
facilities: hotel.facilities
|
||||
.getItems()
|
||||
.map(
|
||||
this.hotelFacilityMapper.mapEntityToResponse.bind(
|
||||
this.hotelFacilityMapper,
|
||||
),
|
||||
),
|
||||
address: hotel.address,
|
||||
landmark: hotel.landmark,
|
||||
distance_to_landmark: hotel.distanceToLandmark,
|
||||
food_type: hotel.foodType,
|
||||
food_amount: hotel.foodAmount,
|
||||
food_menu: hotel.foodMenu,
|
||||
created_at: hotel.createdAt,
|
||||
updated_at: hotel.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,11 @@
|
||||
import z from "zod";
|
||||
|
||||
export const hotelFacilityRequestSchema = z.object({
|
||||
name: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
icon: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
});
|
||||
|
||||
export const hotelRequestSchema = z.object({
|
||||
name: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
city_slug: z
|
||||
city_id: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
@@ -34,20 +23,20 @@ export const hotelRequestSchema = z.object({
|
||||
google_maps_link: z
|
||||
.url("Must be valid string URL.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(500, "Max 500 characters."),
|
||||
.max(1000, "Max 1000 characters."),
|
||||
google_maps_embed: z
|
||||
.url("Must be valid string URL.")
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(500, "Max 500 characters."),
|
||||
.max(1000, "Max 1000 characters."),
|
||||
google_reviews_link: z
|
||||
.url("Must be valid string URL.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(500, "Max 500 characters."),
|
||||
.max(1000, "Max 1000 characters."),
|
||||
description: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(1000, "Max 1000 characters."),
|
||||
facility_slugs: z
|
||||
facility_ids: z
|
||||
.array(
|
||||
z
|
||||
.string("Must be string.")
|
||||
@@ -81,15 +70,8 @@ export const hotelRequestSchema = z.object({
|
||||
.max(100, "Max 100 characters."),
|
||||
});
|
||||
|
||||
export const hotelFacilityQuerySchema = z.object({
|
||||
slug: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
});
|
||||
|
||||
export const hotelQuerySchema = z.object({
|
||||
slug: z
|
||||
export const hotelParamsSchema = z.object({
|
||||
id: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
import type { CityResponse } from "@/modules/city/city.types";
|
||||
import type { HotelFacilityResponse } from "@/modules/hotel-facility/hotel-facility.types";
|
||||
import type {
|
||||
hotelFacilityRequestSchema,
|
||||
hotelQuerySchema,
|
||||
hotelParamsSchema,
|
||||
hotelRequestSchema,
|
||||
} from "@/modules/hotel/hotel.schemas";
|
||||
import z from "zod";
|
||||
|
||||
export type HotelFacilityRequest = z.infer<typeof hotelFacilityRequestSchema>;
|
||||
|
||||
export type HotelRequest = z.infer<typeof hotelRequestSchema>;
|
||||
|
||||
export type HotelFacilityQuery = z.infer<typeof hotelFacilityRequestSchema>;
|
||||
|
||||
export type HotelQuery = z.infer<typeof hotelQuerySchema>;
|
||||
|
||||
export type HotelFacilityResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
export type HotelParams = z.infer<typeof hotelParamsSchema>;
|
||||
|
||||
export type HotelResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
city: CityResponse;
|
||||
star: number;
|
||||
images: string[];
|
||||
google_maps_link: string;
|
||||
google_maps_embed: string;
|
||||
google_reviews_link: string;
|
||||
|
||||
1358
src/modules/package/package.controller.ts
Normal file
1358
src/modules/package/package.controller.ts
Normal file
File diff suppressed because it is too large
Load Diff
216
src/modules/package/package.mapper.ts
Normal file
216
src/modules/package/package.mapper.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import type { FlightSchedule } from "@/database/entities/flight-schedule.entity";
|
||||
import type { HotelSchedule } from "@/database/entities/hotel-schedule.entity";
|
||||
import type { PackageDetail } from "@/database/entities/package-detail.entity";
|
||||
import type { PackageItineraryDay } from "@/database/entities/package-itinerary-day.entity";
|
||||
import type {
|
||||
PackageItineraryWidget,
|
||||
PackageItineraryWidgetHotel,
|
||||
PackageItineraryWidgetInformation,
|
||||
PackageItineraryWidgetTransport,
|
||||
} from "@/database/entities/package-itinerary-widget.entity";
|
||||
import type { PackageItinerary } from "@/database/entities/package-itinerary.entity";
|
||||
import type { Package } from "@/database/entities/package.entity";
|
||||
import { PackageClass } from "@/database/enums/package-class.enum";
|
||||
import { PackageItineraryWidgetType } from "@/database/enums/package-itinerary-widget-type.enum";
|
||||
import { PackageType } from "@/database/enums/package-type.enum";
|
||||
import type { FlightMapper } from "@/modules/flight/flight.mapper";
|
||||
import type { FlightClassResponse } from "@/modules/flight/flight.types";
|
||||
import type { HotelMapper } from "@/modules/hotel/hotel.mapper";
|
||||
import type {
|
||||
PackageDetailResponse,
|
||||
PackageHotelResponse,
|
||||
PackageItineraryDayResponse,
|
||||
PackageItineraryResponse,
|
||||
PackageItineraryWidgetResponse,
|
||||
PackageRequest,
|
||||
PackageResponse,
|
||||
} from "@/modules/package/package.types";
|
||||
import type { TransportationMapper } from "@/modules/transportation/transportation.mapper";
|
||||
import * as dateFns from "date-fns";
|
||||
|
||||
export class PackageMapper {
|
||||
public constructor(
|
||||
private readonly flightMapper: FlightMapper,
|
||||
private readonly hotelMapper: HotelMapper,
|
||||
private readonly transportationMapper: TransportationMapper,
|
||||
) {}
|
||||
|
||||
public mapPackageType(packageType: PackageRequest["type"]): PackageType {
|
||||
switch (packageType) {
|
||||
case "reguler":
|
||||
return PackageType.reguler;
|
||||
case "plus":
|
||||
return PackageType.plus;
|
||||
}
|
||||
}
|
||||
|
||||
public mapPackageClass(packageClass: PackageRequest["class"]): PackageClass {
|
||||
switch (packageClass) {
|
||||
case "silver":
|
||||
return PackageClass.silver;
|
||||
case "gold":
|
||||
return PackageClass.gold;
|
||||
case "platinum":
|
||||
return PackageClass.platinum;
|
||||
}
|
||||
}
|
||||
|
||||
public mapEntityToResponse(package_: Package): PackageResponse {
|
||||
return {
|
||||
id: package_.id,
|
||||
name: package_.name,
|
||||
type: package_.type,
|
||||
class: package_.class,
|
||||
thumbnail: package_.thumbnail,
|
||||
use_fast_train: package_.useFastTrain,
|
||||
created_at: package_.createdAt,
|
||||
updated_at: package_.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private mapFlightSchedule(
|
||||
flightSchedule: FlightSchedule,
|
||||
): FlightClassResponse[] {
|
||||
const flightClassResponses: FlightClassResponse[] = [];
|
||||
|
||||
let currentFlightSchedule: FlightSchedule | null = flightSchedule;
|
||||
while (currentFlightSchedule !== null) {
|
||||
flightClassResponses.push(
|
||||
this.flightMapper.mapClassEntityToResponse(
|
||||
currentFlightSchedule.flight,
|
||||
),
|
||||
);
|
||||
currentFlightSchedule = currentFlightSchedule.next;
|
||||
}
|
||||
|
||||
return flightClassResponses;
|
||||
}
|
||||
|
||||
private mapHotelSchedule(hotelSchedule: HotelSchedule): PackageHotelResponse {
|
||||
console.log(hotelSchedule.checkIn);
|
||||
const checkIn = dateFns.parse(hotelSchedule.checkIn, "H:mm:ss", new Date());
|
||||
|
||||
const checkOut = dateFns.parse(
|
||||
hotelSchedule.checkOut,
|
||||
"H:mm:ss",
|
||||
new Date(),
|
||||
);
|
||||
|
||||
return {
|
||||
hotel: this.hotelMapper.mapEntityToResponse(hotelSchedule.hotel),
|
||||
check_in: dateFns.format(checkIn, "HH:mm"),
|
||||
check_out: dateFns.format(checkOut, "HH:mm"),
|
||||
};
|
||||
}
|
||||
|
||||
private mapItineraryWidget(
|
||||
packageItineraryWidget: PackageItineraryWidget,
|
||||
): PackageItineraryWidgetResponse {
|
||||
switch (packageItineraryWidget.type) {
|
||||
case PackageItineraryWidgetType.transport:
|
||||
const transportWidget =
|
||||
packageItineraryWidget as PackageItineraryWidgetTransport;
|
||||
|
||||
return {
|
||||
type: "transport",
|
||||
transportation: transportWidget.transportation,
|
||||
from: transportWidget.from,
|
||||
to: transportWidget.to,
|
||||
};
|
||||
case PackageItineraryWidgetType.hotel:
|
||||
const hotelWidget =
|
||||
packageItineraryWidget as PackageItineraryWidgetHotel;
|
||||
|
||||
return {
|
||||
type: "hotel",
|
||||
hotel: this.hotelMapper.mapEntityToResponse(hotelWidget.hotel),
|
||||
};
|
||||
case PackageItineraryWidgetType.information:
|
||||
const informationWidget =
|
||||
packageItineraryWidget as PackageItineraryWidgetInformation;
|
||||
|
||||
return {
|
||||
type: "information",
|
||||
description: informationWidget.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private mapItineraryDay(
|
||||
packageItineraryDay: PackageItineraryDay,
|
||||
nth: number,
|
||||
): PackageItineraryDayResponse {
|
||||
return {
|
||||
nth,
|
||||
title: packageItineraryDay.title,
|
||||
description: packageItineraryDay.description,
|
||||
widgets: packageItineraryDay.widgets
|
||||
.getItems()
|
||||
.map((widget) => this.mapItineraryWidget(widget)),
|
||||
};
|
||||
}
|
||||
|
||||
private mapItinerary(
|
||||
packageItinerary: PackageItinerary,
|
||||
nthDay: number = 1,
|
||||
): PackageItineraryResponse {
|
||||
const days: PackageItineraryDayResponse[] = [];
|
||||
for (
|
||||
let currentItineraryDay: PackageItineraryDay | null =
|
||||
packageItinerary.day,
|
||||
index = 0;
|
||||
currentItineraryDay !== null;
|
||||
currentItineraryDay = currentItineraryDay.next, index++
|
||||
) {
|
||||
days.push(this.mapItineraryDay(currentItineraryDay, nthDay + index));
|
||||
}
|
||||
|
||||
return {
|
||||
location: packageItinerary.location,
|
||||
images: packageItinerary.images
|
||||
.getItems()
|
||||
.map((packageItineraryImage) => packageItineraryImage.src),
|
||||
days,
|
||||
};
|
||||
}
|
||||
|
||||
public mapDetailEntityToResponse(
|
||||
packageDetail: PackageDetail,
|
||||
): PackageDetailResponse {
|
||||
const itineraries: PackageItineraryResponse[] = [];
|
||||
for (
|
||||
let currentItinerary: PackageItinerary | null = packageDetail.itinerary,
|
||||
nthDay = 1;
|
||||
currentItinerary !== null;
|
||||
currentItinerary = currentItinerary.next
|
||||
) {
|
||||
const itineraryResponse = this.mapItinerary(currentItinerary, nthDay);
|
||||
itineraries.push(itineraryResponse);
|
||||
nthDay += itineraryResponse.days.length;
|
||||
}
|
||||
|
||||
return {
|
||||
id: packageDetail.id,
|
||||
package: this.mapEntityToResponse(packageDetail.package),
|
||||
departure_date: dateFns.format(packageDetail.departureDate, "yyyy-MM-dd"),
|
||||
tour_flights: packageDetail.tourFlight
|
||||
? this.mapFlightSchedule(packageDetail.tourFlight)
|
||||
: null,
|
||||
outbound_flights: this.mapFlightSchedule(packageDetail.outboundFlight),
|
||||
inbound_flights: this.mapFlightSchedule(packageDetail.inboundFlight),
|
||||
tour_hotels: packageDetail.tourHotels.map(this.mapHotelSchedule),
|
||||
makkah_hotel: this.mapHotelSchedule(packageDetail.makkahHotel),
|
||||
madinah_hotel: this.mapHotelSchedule(packageDetail.madinahHotel),
|
||||
transportation: this.transportationMapper.mapClassEntityToResponse(
|
||||
packageDetail.transportation,
|
||||
),
|
||||
quad_price: packageDetail.quadPrice,
|
||||
triple_price: packageDetail.triplePrice,
|
||||
double_price: packageDetail.doublePrice,
|
||||
infant_price: packageDetail.infantPrice,
|
||||
itineraries,
|
||||
created_at: packageDetail.createdAt,
|
||||
updated_at: packageDetail.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -17,53 +17,57 @@ export const packageRequestSchema = z.object({
|
||||
|
||||
export const packageDetailRequestSchema = z.object({
|
||||
departure_date: dateSchema,
|
||||
tour_flight_slugs: z
|
||||
tour_flight_ids: z
|
||||
.array(
|
||||
z
|
||||
.string("Must be string.")
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(420, "Max 420 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
"Must be array.",
|
||||
)
|
||||
.nonempty("Must not empty.")
|
||||
.nullable(),
|
||||
outbound_flight_ids: z
|
||||
.array(
|
||||
z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(30, "Max 30 characters."),
|
||||
"Must be array.",
|
||||
)
|
||||
.nonempty("Must not empty."),
|
||||
outbound_flight_slugs: z
|
||||
inbound_flight_ids: z
|
||||
.array(
|
||||
z
|
||||
.string("Must be string.")
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(420, "Max 420 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
"Must be array.",
|
||||
)
|
||||
.nonempty("Must not empty."),
|
||||
inbound_flight_slugs: z
|
||||
tour_hotels: z
|
||||
.array(
|
||||
z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(420, "Max 420 characters."),
|
||||
z.object(
|
||||
{
|
||||
hotel_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(30, "Max 30 characters."),
|
||||
check_in: timeSchema,
|
||||
check_out: timeSchema,
|
||||
},
|
||||
"Must be object.",
|
||||
),
|
||||
"Must be array.",
|
||||
)
|
||||
.nonempty("Must not empty."),
|
||||
tour_hotels: z.array(
|
||||
z.object(
|
||||
{
|
||||
hotel_slug: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
check_in: timeSchema,
|
||||
check_out: timeSchema,
|
||||
},
|
||||
"Must be object.",
|
||||
),
|
||||
"Must be array.",
|
||||
),
|
||||
.nonempty("Must not empty.")
|
||||
.nullable(),
|
||||
makkah_hotel: z.object(
|
||||
{
|
||||
hotel_slug: z
|
||||
.string("Must be string.")
|
||||
hotel_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
check_in: timeSchema,
|
||||
check_out: timeSchema,
|
||||
},
|
||||
@@ -71,19 +75,19 @@ export const packageDetailRequestSchema = z.object({
|
||||
),
|
||||
madinah_hotel: z.object(
|
||||
{
|
||||
hotel_slug: z
|
||||
.string("Must be string.")
|
||||
hotel_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
check_in: timeSchema,
|
||||
check_out: timeSchema,
|
||||
},
|
||||
"Must be object.",
|
||||
),
|
||||
transportation_slug: z
|
||||
.string("Must be string.")
|
||||
transportation_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
quad_price: z
|
||||
.number("Must be number.")
|
||||
.int("Must be integer.")
|
||||
@@ -101,22 +105,97 @@ export const packageDetailRequestSchema = z.object({
|
||||
.int("Must be integer.")
|
||||
.positive("Must be positive.")
|
||||
.nullable(),
|
||||
itineraries: z.array(
|
||||
z.object(
|
||||
{
|
||||
location: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
images: z.array(
|
||||
z.base64("Must be base64 string.").nonempty("Must not empty."),
|
||||
"Must be array.",
|
||||
),
|
||||
days: z.array(
|
||||
z.object(
|
||||
{
|
||||
title: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
description: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(1000, "Max 1000 characters."),
|
||||
widgets: z.array(
|
||||
z.discriminatedUnion("type", [
|
||||
z.object(
|
||||
{
|
||||
type: z.literal("transport"),
|
||||
transportation: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
from: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
to: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
},
|
||||
"Must be object.",
|
||||
),
|
||||
z.object(
|
||||
{
|
||||
type: z.literal("hotel"),
|
||||
hotel_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(30, "Max 30 characters."),
|
||||
},
|
||||
"Must be object.",
|
||||
),
|
||||
z.object(
|
||||
{
|
||||
type: z.literal("information"),
|
||||
description: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(1000, "Max 1000 characters."),
|
||||
},
|
||||
"Must be object.",
|
||||
),
|
||||
]),
|
||||
"Must be array.",
|
||||
),
|
||||
},
|
||||
"Must be object.",
|
||||
),
|
||||
"Must be array.",
|
||||
),
|
||||
},
|
||||
"Must be object.",
|
||||
),
|
||||
"Must be array.",
|
||||
),
|
||||
});
|
||||
|
||||
export const packageQuerySchema = z.object({
|
||||
detail_slug: z
|
||||
.string("Must be string.")
|
||||
export const packageParamsSchema = z.object({
|
||||
id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
slug: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
|
||||
export const packageDetailQuerySchema = z.object({
|
||||
slug: z
|
||||
.string("Must be string.")
|
||||
export const packageDetailParamsSchema = z.object({
|
||||
package_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { FlightClassResponse } from "@/modules/flight/flight.types";
|
||||
import type { HotelResponse } from "@/modules/hotel/hotel.types";
|
||||
import type {
|
||||
packageDetailQuerySchema,
|
||||
packageDetailParamsSchema,
|
||||
packageDetailRequestSchema,
|
||||
packageQuerySchema,
|
||||
packageParamsSchema,
|
||||
packageRequestSchema,
|
||||
} from "@/modules/package/package.schemas";
|
||||
import type { TransportationClassResponse } from "@/modules/transportation/transportation.types";
|
||||
@@ -13,13 +13,12 @@ export type PackageRequest = z.infer<typeof packageRequestSchema>;
|
||||
|
||||
export type PackageDetailRequest = z.infer<typeof packageDetailRequestSchema>;
|
||||
|
||||
export type PackageQuery = z.infer<typeof packageQuerySchema>;
|
||||
export type PackageParams = z.infer<typeof packageParamsSchema>;
|
||||
|
||||
export type PackageDetailQuery = z.infer<typeof packageDetailQuerySchema>;
|
||||
export type PackageDetailParams = z.infer<typeof packageDetailParamsSchema>;
|
||||
|
||||
export type PackageResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
type: "reguler" | "plus";
|
||||
class: "silver" | "gold" | "platinum";
|
||||
@@ -35,22 +34,51 @@ export type PackageHotelResponse = {
|
||||
check_out: string;
|
||||
};
|
||||
|
||||
export type PackageItineraryWidgetResponse =
|
||||
| {
|
||||
type: "transport";
|
||||
transportation: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
| {
|
||||
type: "hotel";
|
||||
hotel: HotelResponse;
|
||||
}
|
||||
| {
|
||||
type: "information";
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type PackageItineraryDayResponse = {
|
||||
nth: number;
|
||||
title: string;
|
||||
description: string;
|
||||
widgets: PackageItineraryWidgetResponse[];
|
||||
};
|
||||
|
||||
export type PackageItineraryResponse = {
|
||||
location: string;
|
||||
images: string[];
|
||||
days: PackageItineraryDayResponse[];
|
||||
};
|
||||
|
||||
export type PackageDetailResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
package: PackageResponse;
|
||||
departure_date: string;
|
||||
tour_flights: FlightClassResponse[];
|
||||
tour_flights: FlightClassResponse[] | null;
|
||||
outbound_flights: FlightClassResponse[];
|
||||
inbound_flights: FlightClassResponse[];
|
||||
tour_hotels: PackageHotelResponse[];
|
||||
tour_hotels: PackageHotelResponse[] | null;
|
||||
makkah_hotel: PackageHotelResponse;
|
||||
medina_hotel: PackageHotelResponse;
|
||||
madinah_hotel: PackageHotelResponse;
|
||||
transportation: TransportationClassResponse;
|
||||
quad_price: number;
|
||||
triple_price: number;
|
||||
double_price: number;
|
||||
infant_price: number | null;
|
||||
itineraries: PackageItineraryResponse[];
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
|
||||
554
src/modules/transportation/transportation.controller.ts
Normal file
554
src/modules/transportation/transportation.controller.ts
Normal file
@@ -0,0 +1,554 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { TransportationClass } from "@/database/entities/transportation-class.entity";
|
||||
import { TransportationImage } from "@/database/entities/transportation-image.entity";
|
||||
import { Transportation } from "@/database/entities/transportation.entity";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { TransportationMapper } from "@/modules/transportation/transportation.mapper";
|
||||
import {
|
||||
transportationClassParamsSchema,
|
||||
transportationClassRequestSchema,
|
||||
transportationParamsSchema,
|
||||
transportationRequestSchema,
|
||||
} from "@/modules/transportation/transportation.schemas";
|
||||
import type {
|
||||
TransportationClassResponse,
|
||||
TransportationResponse,
|
||||
} from "@/modules/transportation/transportation.types";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class TransportationController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: TransportationMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = transportationRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const transportation = orm.em.create(Transportation, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
type: body.type,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: this.mapper.mapEntityToResponse(transportation),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<TransportationResponse>);
|
||||
}
|
||||
|
||||
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(Transportation);
|
||||
|
||||
const transportations = await orm.em.find(
|
||||
Transportation,
|
||||
{},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: transportations.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<TransportationResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = transportationParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const transportation = await orm.em.findOne(
|
||||
Transportation,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!transportation) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Transportation not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(transportation),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<TransportationResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = transportationParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = transportationRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const transportation = await orm.em.findOne(
|
||||
Transportation,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!transportation) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Transportation not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
transportation.name = body.name;
|
||||
transportation.type = body.type;
|
||||
transportation.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(transportation),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<TransportationResponse>);
|
||||
}
|
||||
|
||||
async delete(req: Request, res: Response) {
|
||||
const parseParamsResult = transportationParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const transportation = await orm.em.findOne(
|
||||
Transportation,
|
||||
{
|
||||
id: params.id,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!transportation) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Transportation not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(transportation);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
async createClass(req: Request, res: Response) {
|
||||
const parseParamsResult = transportationParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = transportationClassRequestSchema.safeParse(
|
||||
req.body,
|
||||
);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const transportation = await orm.em.findOne(
|
||||
Transportation,
|
||||
{ id: params.id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!transportation) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Transportation not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const transportationClass = orm.em.create(TransportationClass, {
|
||||
id: ulid(),
|
||||
transportation,
|
||||
class: body.class,
|
||||
totalSeats: body.total_seats,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
for (const [index, image] of body.images.entries()) {
|
||||
const imageFile = await this.fileStorage.storeFile(
|
||||
Buffer.from(image, "base64"),
|
||||
);
|
||||
|
||||
const transportationImage = orm.em.create(TransportationImage, {
|
||||
id: ulid(),
|
||||
transportation: transportationClass,
|
||||
src: imageFile.name,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
transportationClass.images.add(transportationImage);
|
||||
}
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: this.mapper.mapClassEntityToResponse(transportationClass),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<TransportationClassResponse>);
|
||||
}
|
||||
|
||||
async listClasses(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 parseParamsResult = transportationParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const transportation = await orm.em.findOne(
|
||||
Transportation,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!transportation) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Transportation not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const count = await orm.em.count(TransportationClass, {
|
||||
transportation,
|
||||
});
|
||||
|
||||
const classes = await orm.em.find(
|
||||
TransportationClass,
|
||||
{ transportation },
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: classes.map(this.mapper.mapClassEntityToResponse.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<TransportationClassResponse>);
|
||||
}
|
||||
|
||||
async viewClass(req: Request, res: Response) {
|
||||
const parseParamsResult = transportationClassParamsSchema.safeParse(
|
||||
req.params,
|
||||
);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const transportation = await orm.em.findOne(
|
||||
Transportation,
|
||||
{ id: params.transportation_id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!transportation) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Transportation not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const transportationClass = await orm.em.findOne(
|
||||
TransportationClass,
|
||||
{
|
||||
id: params.id,
|
||||
transportation,
|
||||
},
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!transportationClass) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Transportation class not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapClassEntityToResponse(transportationClass),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<TransportationClassResponse>);
|
||||
}
|
||||
|
||||
async updateClass(req: Request, res: Response) {
|
||||
const parseParamsResult = transportationClassParamsSchema.safeParse(
|
||||
req.params,
|
||||
);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = transportationClassRequestSchema.safeParse(
|
||||
req.body,
|
||||
);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const transportation = await orm.em.findOne(
|
||||
Transportation,
|
||||
{ id: params.transportation_id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!transportation) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "transportation_id",
|
||||
location: "params",
|
||||
message: "Transportation not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const transportationClass = await orm.em.findOne(
|
||||
TransportationClass,
|
||||
{
|
||||
id: params.id,
|
||||
transportation,
|
||||
},
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!transportationClass) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Transportation class not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
transportationClass.class = body.class;
|
||||
transportationClass.totalSeats = body.total_seats;
|
||||
transportationClass.updatedAt = new Date();
|
||||
|
||||
for (const transportationImage of transportationClass.images) {
|
||||
await this.fileStorage.removeFile(transportationImage.src);
|
||||
|
||||
orm.em.remove(transportationImage);
|
||||
}
|
||||
for (const [index, image] of body.images.entries()) {
|
||||
const imageFile = await this.fileStorage.storeFile(
|
||||
Buffer.from(image, "base64"),
|
||||
);
|
||||
|
||||
const transportationImage = orm.em.create(TransportationImage, {
|
||||
id: ulid(),
|
||||
transportation: transportationClass,
|
||||
src: imageFile.name,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
transportationClass.images.add(transportationImage);
|
||||
}
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapClassEntityToResponse(transportationClass),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<TransportationClassResponse>);
|
||||
}
|
||||
|
||||
async deleteClass(req: Request, res: Response) {
|
||||
const parseParamsResult = transportationClassParamsSchema.safeParse(
|
||||
req.params,
|
||||
);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const transportation = await orm.em.findOne(
|
||||
Transportation,
|
||||
{ id: params.transportation_id },
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!transportation) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "transportation_id",
|
||||
location: "params",
|
||||
message: "Transportation not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const transportationClass = await orm.em.findOne(
|
||||
TransportationClass,
|
||||
{
|
||||
id: params.id,
|
||||
transportation,
|
||||
},
|
||||
{ populate: ["*"] },
|
||||
);
|
||||
if (!transportationClass) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Transportation class not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
for (const image of transportationClass.images.getItems()) {
|
||||
await this.fileStorage.removeFile(image.src);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(transportationClass);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post("/:id/classes", ormMiddleware, this.createClass.bind(this));
|
||||
router.get("/:id/classes", ormMiddleware, this.listClasses.bind(this));
|
||||
router.get(
|
||||
"/:transportation_id/classes/:id",
|
||||
ormMiddleware,
|
||||
this.viewClass.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:transportation_id/classes/:id",
|
||||
ormMiddleware,
|
||||
this.updateClass.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:transportation_id/classes/:id",
|
||||
ormMiddleware,
|
||||
this.deleteClass.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
36
src/modules/transportation/transportation.mapper.ts
Normal file
36
src/modules/transportation/transportation.mapper.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { TransportationClass } from "@/database/entities/transportation-class.entity";
|
||||
import type { Transportation } from "@/database/entities/transportation.entity";
|
||||
import type {
|
||||
TransportationClassResponse,
|
||||
TransportationResponse,
|
||||
} from "@/modules/transportation/transportation.types";
|
||||
|
||||
export class TransportationMapper {
|
||||
public mapEntityToResponse(
|
||||
transportation: Transportation,
|
||||
): TransportationResponse {
|
||||
return {
|
||||
id: transportation.id,
|
||||
name: transportation.name,
|
||||
type: transportation.type,
|
||||
created_at: transportation.createdAt,
|
||||
updated_at: transportation.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
public mapClassEntityToResponse(
|
||||
transportationClass: TransportationClass,
|
||||
): TransportationClassResponse {
|
||||
return {
|
||||
id: transportationClass.id,
|
||||
transportation: this.mapEntityToResponse(
|
||||
transportationClass.transportation,
|
||||
),
|
||||
class: transportationClass.class,
|
||||
total_seats: transportationClass.totalSeats,
|
||||
images: transportationClass.images.getItems().map((image) => image.src),
|
||||
created_at: transportationClass.createdAt,
|
||||
updated_at: transportationClass.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -28,20 +28,20 @@ export const transportationClassRequestSchema = z.object({
|
||||
.nonempty("Must not empty."),
|
||||
});
|
||||
|
||||
export const transportationQuerySchema = z.object({
|
||||
slug: z
|
||||
.string("Must be string.")
|
||||
export const transportationParamsSchema = z.object({
|
||||
id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
|
||||
export const transportationClassQuerySchema = z.object({
|
||||
transportation_slug: z
|
||||
.string("Must be string.")
|
||||
export const transportationClassParamsSchema = z.object({
|
||||
transportation_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
slug: z
|
||||
.string("Must be string.")
|
||||
.max(30, "Max 30 characters."),
|
||||
id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(400, "Max 400 characters."),
|
||||
.max(30, "Max 30 characters."),
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
transportationClassQuerySchema,
|
||||
transportationClassParamsSchema,
|
||||
transportationClassRequestSchema,
|
||||
transportationQuerySchema,
|
||||
transportationParamsSchema,
|
||||
transportationRequestSchema,
|
||||
} from "@/modules/transportation/transportation.schemas";
|
||||
import z from "zod";
|
||||
@@ -12,23 +12,22 @@ export type TransportationClassRequest = z.infer<
|
||||
typeof transportationClassRequestSchema
|
||||
>;
|
||||
|
||||
export type TransportationQuery = z.infer<typeof transportationQuerySchema>;
|
||||
export type TransportationParams = z.infer<typeof transportationParamsSchema>;
|
||||
|
||||
export type TransportationClassQuery = z.infer<
|
||||
typeof transportationClassQuerySchema
|
||||
export type TransportationClassParams = z.infer<
|
||||
typeof transportationClassParamsSchema
|
||||
>;
|
||||
|
||||
export type TransportationResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
type: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
|
||||
export type TransportationClassResponse = {
|
||||
id: string;
|
||||
slug: string;
|
||||
transportation: TransportationResponse;
|
||||
class: string;
|
||||
total_seats: number;
|
||||
|
||||
Reference in New Issue
Block a user