add core api
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
storage
|
||||||
|
.env
|
||||||
|
|||||||
6
bun.lock
6
bun.lock
@@ -14,6 +14,8 @@
|
|||||||
"file-type": "21.0.0",
|
"file-type": "21.0.0",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
|
"slugify": "1.6.6",
|
||||||
|
"ulid": "3.0.1",
|
||||||
"zod": "4.1.12",
|
"zod": "4.1.12",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -401,6 +403,8 @@
|
|||||||
|
|
||||||
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||||
|
|
||||||
|
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
|
||||||
|
|
||||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||||
@@ -443,6 +447,8 @@
|
|||||||
|
|
||||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||||
|
|
||||||
|
"ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="],
|
||||||
|
|
||||||
"umzug": ["umzug@3.8.2", "", { "dependencies": { "@rushstack/ts-command-line": "^4.12.2", "emittery": "^0.13.0", "fast-glob": "^3.3.2", "pony-cause": "^2.1.4", "type-fest": "^4.0.0" } }, "sha512-BEWEF8OJjTYVC56GjELeHl/1XjFejrD7aHzn+HldRJTx+pL1siBrKHZC8n4K/xL3bEzVA9o++qD1tK2CpZu4KA=="],
|
"umzug": ["umzug@3.8.2", "", { "dependencies": { "@rushstack/ts-command-line": "^4.12.2", "emittery": "^0.13.0", "fast-glob": "^3.3.2", "pony-cause": "^2.1.4", "type-fest": "^4.0.0" } }, "sha512-BEWEF8OJjTYVC56GjELeHl/1XjFejrD7aHzn+HldRJTx+pL1siBrKHZC8n4K/xL3bEzVA9o++qD1tK2CpZu4KA=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run src/server.ts"
|
"dev": "bun run src/server.ts",
|
||||||
|
"dev:watch": "bun run --watch src/server.ts"
|
||||||
},
|
},
|
||||||
"mikro-orm": {
|
"mikro-orm": {
|
||||||
"configPaths": [
|
"configPaths": [
|
||||||
@@ -19,6 +20,8 @@
|
|||||||
"file-type": "21.0.0",
|
"file-type": "21.0.0",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
|
"slugify": "1.6.6",
|
||||||
|
"ulid": "3.0.1",
|
||||||
"zod": "4.1.12"
|
"zod": "4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,4 +1,24 @@
|
|||||||
|
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||||
|
import { LocalFileStorage } from "@/common/services/file-storage/local.file-storage";
|
||||||
import { serverConfig } from "@/configs/server.config";
|
import { serverConfig } from "@/configs/server.config";
|
||||||
|
import { AirlineController } from "@/modules/airline/airline.controller";
|
||||||
|
import { AirlineMapper } from "@/modules/airline/airline.mapper";
|
||||||
|
import { AirportController } from "@/modules/airport/airport.controller";
|
||||||
|
import { AirportMapper } from "@/modules/airport/airport.mapper";
|
||||||
|
import { CityController } from "@/modules/city/city.controller";
|
||||||
|
import { CityMapper } from "@/modules/city/city.mapper";
|
||||||
|
import { CountryController } from "@/modules/country/country.controller";
|
||||||
|
import { CountryMapper } from "@/modules/country/country.mapper";
|
||||||
|
import { FlightController } from "@/modules/flight/flight.controller";
|
||||||
|
import { FlightMapper } from "@/modules/flight/flight.mapper";
|
||||||
|
import { HotelFacilityController } from "@/modules/hotel-facility/hotel-facility.controller";
|
||||||
|
import { HotelFacilityMapper } from "@/modules/hotel-facility/hotel-facility.mapper";
|
||||||
|
import { HotelController } from "@/modules/hotel/hotel.controller";
|
||||||
|
import { HotelMapper } from "@/modules/hotel/hotel.mapper";
|
||||||
|
import { PackageController } from "@/modules/package/package.controller";
|
||||||
|
import { PackageMapper } from "@/modules/package/package.mapper";
|
||||||
|
import { TransportationController } from "@/modules/transportation/transportation.controller";
|
||||||
|
import { TransportationMapper } from "@/modules/transportation/transportation.mapper";
|
||||||
import compression from "compression";
|
import compression from "compression";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
@@ -7,28 +27,73 @@ import helmet from "helmet";
|
|||||||
export class Application {
|
export class Application {
|
||||||
private readonly _app: express.Application;
|
private readonly _app: express.Application;
|
||||||
|
|
||||||
|
// Services
|
||||||
|
private _fileStorage!: AbstractFileStorage;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this._app = express();
|
this._app = express();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public initializeServices() {
|
||||||
|
this._fileStorage = new LocalFileStorage();
|
||||||
|
}
|
||||||
|
|
||||||
public initializeMiddlewares() {
|
public initializeMiddlewares() {
|
||||||
this._app.use(helmet());
|
this._app.use(helmet());
|
||||||
this._app.use(cors());
|
this._app.use(cors());
|
||||||
this._app.use(compression());
|
this._app.use(compression());
|
||||||
this._app.use(express.json());
|
this._app.use(express.json({ limit: "1mb" }));
|
||||||
this._app.use(express.urlencoded());
|
this._app.use(express.urlencoded());
|
||||||
}
|
}
|
||||||
|
|
||||||
public initializeRouters() {
|
public initializeRouters() {
|
||||||
// this._app.use("/countries");
|
const countryMapper = new CountryMapper();
|
||||||
// this._app.use("/cities");
|
const cityMapper = new CityMapper(countryMapper);
|
||||||
// this._app.use("/airlines");
|
const airlineMapper = new AirlineMapper();
|
||||||
// this._app.use("/airports");
|
const airportMapper = new AirportMapper(cityMapper);
|
||||||
// this._app.use("/flights");
|
const flightMapper = new FlightMapper(airlineMapper, airportMapper);
|
||||||
// this._app.use("/hotel-facilities");
|
const hotelFacilityMapper = new HotelFacilityMapper();
|
||||||
// this._app.use("/hotels");
|
const hotelMapper = new HotelMapper(cityMapper, hotelFacilityMapper);
|
||||||
// this._app.use("/transportations");
|
const transportationMapper = new TransportationMapper();
|
||||||
// this._app.use("/packages");
|
const packageMapper = new PackageMapper(
|
||||||
|
flightMapper,
|
||||||
|
hotelMapper,
|
||||||
|
transportationMapper,
|
||||||
|
);
|
||||||
|
|
||||||
|
const countryRouter = new CountryController(countryMapper).buildRouter();
|
||||||
|
const cityRouter = new CityController(cityMapper).buildRouter();
|
||||||
|
const airlineRouter = new AirlineController(
|
||||||
|
airlineMapper,
|
||||||
|
this._fileStorage,
|
||||||
|
).buildRouter();
|
||||||
|
const airportRouter = new AirportController(airportMapper).buildRouter();
|
||||||
|
const flightRouter = new FlightController(flightMapper).buildRouter();
|
||||||
|
const hotelFacilityRouter = new HotelFacilityController(
|
||||||
|
hotelFacilityMapper,
|
||||||
|
).buildRouter();
|
||||||
|
const hotelRouter = new HotelController(
|
||||||
|
hotelMapper,
|
||||||
|
this._fileStorage,
|
||||||
|
).buildRouter();
|
||||||
|
const transportationRouter = new TransportationController(
|
||||||
|
transportationMapper,
|
||||||
|
this._fileStorage,
|
||||||
|
).buildRouter();
|
||||||
|
const packageRouter = new PackageController(
|
||||||
|
packageMapper,
|
||||||
|
this._fileStorage,
|
||||||
|
).buildRouter();
|
||||||
|
|
||||||
|
this._app.use("/countries", countryRouter);
|
||||||
|
this._app.use("/cities", cityRouter);
|
||||||
|
this._app.use("/airlines", airlineRouter);
|
||||||
|
this._app.use("/airports", airportRouter);
|
||||||
|
this._app.use("/flights", flightRouter);
|
||||||
|
this._app.use("/hotel-facilities", hotelFacilityRouter);
|
||||||
|
this._app.use("/hotels", hotelRouter);
|
||||||
|
this._app.use("/transportations", transportationRouter);
|
||||||
|
this._app.use("/packages", packageRouter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public initializeErrorHandlers() {}
|
public initializeErrorHandlers() {}
|
||||||
|
|||||||
22
src/common/controller.ts
Normal file
22
src/common/controller.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { ErrorResponse } from "@/common/types";
|
||||||
|
import type { Response, Router } from "express";
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
|
export abstract class Controller {
|
||||||
|
protected handleZodError(
|
||||||
|
zodError: z.ZodError,
|
||||||
|
res: Response,
|
||||||
|
location: string = "body",
|
||||||
|
) {
|
||||||
|
res.status(422).json({
|
||||||
|
data: null,
|
||||||
|
errors: zodError.issues.map((issue) => ({
|
||||||
|
path: issue.path.join("."),
|
||||||
|
location,
|
||||||
|
message: issue.message,
|
||||||
|
})),
|
||||||
|
} satisfies ErrorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract buildRouter(): Router;
|
||||||
|
}
|
||||||
5
src/common/errors/file-not-found.error.ts
Normal file
5
src/common/errors/file-not-found.error.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export class FileNotFoundError extends Error {
|
||||||
|
public constructor(fileName: string) {
|
||||||
|
super(`File '${fileName}' not found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/common/errors/invalid-file-buffer.error.ts
Normal file
8
src/common/errors/invalid-file-buffer.error.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export class InvalidFileBufferError extends Error {
|
||||||
|
public constructor(
|
||||||
|
public readonly buffer: Buffer,
|
||||||
|
message: string,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/common/middlewares/orm.middleware.ts
Normal file
11
src/common/middlewares/orm.middleware.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { orm } from "@/database/orm";
|
||||||
|
import { RequestContext } from "@mikro-orm/core";
|
||||||
|
import type { NextFunction, Request, Response } from "express";
|
||||||
|
|
||||||
|
export function ormMiddleware(
|
||||||
|
_req: Request,
|
||||||
|
_res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
) {
|
||||||
|
RequestContext.create(orm.em, next);
|
||||||
|
}
|
||||||
@@ -1,6 +1,19 @@
|
|||||||
import { isValid, parse } from "date-fns";
|
import { isValid, parse } from "date-fns";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
|
export const paginationQuerySchema = z.object({
|
||||||
|
page: z.coerce
|
||||||
|
.number("Must be number.")
|
||||||
|
.int("Must be integer.")
|
||||||
|
.min(1, "Minimum 1.")
|
||||||
|
.default(1),
|
||||||
|
per_page: z.coerce
|
||||||
|
.number("Must be number.")
|
||||||
|
.int("Must be integer.")
|
||||||
|
.min(1, "Minimum 1.")
|
||||||
|
.default(100),
|
||||||
|
});
|
||||||
|
|
||||||
export const timeSchema = z
|
export const timeSchema = z
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not be empty.")
|
.nonempty("Must not be empty.")
|
||||||
@@ -14,21 +27,21 @@ export const timeSchema = z
|
|||||||
});
|
});
|
||||||
return z.NEVER;
|
return z.NEVER;
|
||||||
}
|
}
|
||||||
return val;
|
return parsedDate;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dateSchema = z
|
export const dateSchema = z
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not be empty.")
|
.nonempty("Must not be empty.")
|
||||||
.transform((val, ctx) => {
|
.transform((val, ctx) => {
|
||||||
const parsedDate = parse(val, "YYYY-MM-DD", new Date());
|
const parsedDate = parse(val, "yyyy-MM-dd", new Date());
|
||||||
if (!isValid(parsedDate)) {
|
if (!isValid(parsedDate)) {
|
||||||
ctx.issues.push({
|
ctx.issues.push({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
message: "Must be in 'YYYY-MM-DD' format.",
|
message: "Must be in 'yyyy-MM-dd' format.",
|
||||||
input: val,
|
input: val,
|
||||||
});
|
});
|
||||||
return z.NEVER;
|
return z.NEVER;
|
||||||
}
|
}
|
||||||
return val;
|
return parsedDate;
|
||||||
});
|
});
|
||||||
|
|||||||
14
src/common/services/file-storage/abstract.file-storage.ts
Normal file
14
src/common/services/file-storage/abstract.file-storage.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export type FileResult = {
|
||||||
|
name: string;
|
||||||
|
mimeType: string;
|
||||||
|
extension: string;
|
||||||
|
buffer: Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class AbstractFileStorage {
|
||||||
|
public abstract storeFile(buffer: Buffer, name?: string): Promise<FileResult>;
|
||||||
|
|
||||||
|
public abstract retrieveFile(name: string): Promise<FileResult>;
|
||||||
|
|
||||||
|
public abstract removeFile(name: string): Promise<void>;
|
||||||
|
}
|
||||||
59
src/common/services/file-storage/local.file-storage.ts
Normal file
59
src/common/services/file-storage/local.file-storage.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { InvalidFileBufferError } from "@/common/errors/invalid-file-buffer.error";
|
||||||
|
import {
|
||||||
|
AbstractFileStorage,
|
||||||
|
type FileResult,
|
||||||
|
} from "@/common/services/file-storage/abstract.file-storage";
|
||||||
|
import { fileTypeFromBuffer } from "file-type";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const STORAGE_DIRECTORY = join(process.cwd(), "storage");
|
||||||
|
|
||||||
|
export class LocalFileStorage extends AbstractFileStorage {
|
||||||
|
public constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async storeFile(buffer: Buffer, name?: string): Promise<FileResult> {
|
||||||
|
const fileType = await fileTypeFromBuffer(buffer);
|
||||||
|
if (!fileType) {
|
||||||
|
throw new InvalidFileBufferError(
|
||||||
|
buffer,
|
||||||
|
"Unable to determine file type from buffer.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = name ?? `${Date.now()}.${fileType.ext}`;
|
||||||
|
|
||||||
|
const filePath = join(STORAGE_DIRECTORY, fileName);
|
||||||
|
|
||||||
|
await Bun.write(filePath, buffer, { createPath: true });
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: fileName,
|
||||||
|
buffer,
|
||||||
|
mimeType: fileType.mime,
|
||||||
|
extension: fileType.ext,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async retrieveFile(name: string): Promise<FileResult> {
|
||||||
|
const filePath = join(STORAGE_DIRECTORY, name);
|
||||||
|
|
||||||
|
const file = Bun.file(filePath);
|
||||||
|
|
||||||
|
const fileName = file.name ?? name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: fileName,
|
||||||
|
buffer: Buffer.from(await file.arrayBuffer()),
|
||||||
|
mimeType: file.type,
|
||||||
|
extension: fileName.split(".").pop() ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeFile(name: string): Promise<void> {
|
||||||
|
const filePath = join(STORAGE_DIRECTORY, name);
|
||||||
|
|
||||||
|
await Bun.file(filePath).delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/common/types.ts
Normal file
29
src/common/types.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { paginationQuerySchema } from "@/common/schemas";
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
|
export type PaginationQuery = z.infer<typeof paginationQuerySchema>;
|
||||||
|
|
||||||
|
export type SingleResponse<T> = {
|
||||||
|
data: T;
|
||||||
|
errors: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListResponse<T> = {
|
||||||
|
data: T[];
|
||||||
|
errors: null;
|
||||||
|
meta: {
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
total_pages: number;
|
||||||
|
total_items: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ErrorResponse = {
|
||||||
|
data: null;
|
||||||
|
errors: {
|
||||||
|
path?: string;
|
||||||
|
location?: string;
|
||||||
|
message: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
@@ -1,24 +1,11 @@
|
|||||||
import { Flight } from "@/database/entities/flight.entity";
|
|
||||||
import { SkytraxType } from "@/database/enums/skytrax-type.enum";
|
import { SkytraxType } from "@/database/enums/skytrax-type.enum";
|
||||||
import {
|
import { Entity, Enum, PrimaryKey, Property, Unique } from "@mikro-orm/core";
|
||||||
Collection,
|
|
||||||
Entity,
|
|
||||||
Enum,
|
|
||||||
OneToMany,
|
|
||||||
PrimaryKey,
|
|
||||||
Property,
|
|
||||||
Unique,
|
|
||||||
} from "@mikro-orm/core";
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Airline {
|
export class Airline {
|
||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 200 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@@ -30,7 +17,7 @@ export class Airline {
|
|||||||
@Unique()
|
@Unique()
|
||||||
logo!: string;
|
logo!: string;
|
||||||
|
|
||||||
@Property({ type: "int", nullable: true })
|
@Property({ type: "int", unsigned: true })
|
||||||
skytraxRating!: number;
|
skytraxRating!: number;
|
||||||
|
|
||||||
@Enum(() => SkytraxType)
|
@Enum(() => SkytraxType)
|
||||||
@@ -48,9 +35,4 @@ export class Airline {
|
|||||||
onUpdate: () => new Date(),
|
onUpdate: () => new Date(),
|
||||||
})
|
})
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
// Collections
|
|
||||||
|
|
||||||
@OneToMany(() => Flight, (flight) => flight.airline)
|
|
||||||
flights = new Collection<Flight>(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { City } from "@/database/entities/city.entity";
|
import { City } from "@/database/entities/city.entity";
|
||||||
import { Flight } from "@/database/entities/flight.entity";
|
|
||||||
import {
|
import {
|
||||||
Collection,
|
|
||||||
Entity,
|
Entity,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
type Rel,
|
type Rel,
|
||||||
@@ -16,10 +13,6 @@ export class Airport {
|
|||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 200 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@@ -42,12 +35,4 @@ export class Airport {
|
|||||||
onUpdate: () => new Date(),
|
onUpdate: () => new Date(),
|
||||||
})
|
})
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
// Collections
|
|
||||||
|
|
||||||
@OneToMany(() => Flight, (flight) => flight.departureAirport)
|
|
||||||
departureFlights = new Collection<Flight>(this);
|
|
||||||
|
|
||||||
@OneToMany(() => Flight, (flight) => flight.arrivalAirport)
|
|
||||||
arrivalFlights = new Collection<Flight>(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { Airport } from "@/database/entities/airport.entity";
|
|
||||||
import { Country } from "@/database/entities/country.entity";
|
import { Country } from "@/database/entities/country.entity";
|
||||||
import { Hotel } from "@/database/entities/hotel.entity";
|
|
||||||
import {
|
import {
|
||||||
Collection,
|
|
||||||
Entity,
|
Entity,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
Unique,
|
|
||||||
type Rel,
|
type Rel,
|
||||||
} from "@mikro-orm/core";
|
} from "@mikro-orm/core";
|
||||||
|
|
||||||
@@ -17,10 +12,6 @@ export class City {
|
|||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 200 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@@ -39,12 +30,4 @@ export class City {
|
|||||||
onUpdate: () => new Date(),
|
onUpdate: () => new Date(),
|
||||||
})
|
})
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
// Collections
|
|
||||||
|
|
||||||
@OneToMany(() => Airport, (airport) => airport.city)
|
|
||||||
cities = new Collection<City>(this);
|
|
||||||
|
|
||||||
@OneToMany(() => Hotel, (hotel) => hotel.city)
|
|
||||||
hotels = new Collection<Hotel>(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
import { City } from "@/database/entities/city.entity";
|
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
|
||||||
import {
|
|
||||||
Collection,
|
|
||||||
Entity,
|
|
||||||
OneToMany,
|
|
||||||
PrimaryKey,
|
|
||||||
Property,
|
|
||||||
Unique,
|
|
||||||
} from "@mikro-orm/core";
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Country {
|
export class Country {
|
||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 200 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@@ -32,9 +20,4 @@ export class Country {
|
|||||||
onUpdate: () => new Date(),
|
onUpdate: () => new Date(),
|
||||||
})
|
})
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
// Collections
|
|
||||||
|
|
||||||
@OneToMany(() => City, (city) => city.country)
|
|
||||||
cities = new Collection<City>(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Flight } from "@/database/entities/flight.entity";
|
import { Flight } from "@/database/entities/flight.entity";
|
||||||
import { PackageDetail } from "@/database/entities/package-detail.entity";
|
|
||||||
import {
|
import {
|
||||||
Collection,
|
|
||||||
Entity,
|
Entity,
|
||||||
ManyToMany,
|
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
@@ -17,10 +14,6 @@ export class FlightClass {
|
|||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 200 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Flight)
|
@ManyToOne(() => Flight)
|
||||||
flight!: Rel<Flight>;
|
flight!: Rel<Flight>;
|
||||||
|
|
||||||
@@ -48,24 +41,4 @@ export class FlightClass {
|
|||||||
onUpdate: () => new Date(),
|
onUpdate: () => new Date(),
|
||||||
})
|
})
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
// Collections
|
|
||||||
|
|
||||||
@ManyToMany(
|
|
||||||
() => PackageDetail,
|
|
||||||
(packageDetail) => packageDetail.tourFlightClasses,
|
|
||||||
)
|
|
||||||
tourPackageDetails = new Collection<PackageDetail>(this);
|
|
||||||
|
|
||||||
@ManyToMany(
|
|
||||||
() => PackageDetail,
|
|
||||||
(packageDetail) => packageDetail.outboundFlightClasses,
|
|
||||||
)
|
|
||||||
outboundPackageDetails = new Collection<PackageDetail>(this);
|
|
||||||
|
|
||||||
@ManyToMany(
|
|
||||||
() => PackageDetail,
|
|
||||||
(packageDetail) => packageDetail.inboundFlightClasses,
|
|
||||||
)
|
|
||||||
inboundPackageDetails = new Collection<PackageDetail>(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/database/entities/flight-schedule.entity.ts
Normal file
37
src/database/entities/flight-schedule.entity.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { FlightClass } from "@/database/entities/flight-class.entity";
|
||||||
|
import {
|
||||||
|
Cascade,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryKey,
|
||||||
|
Property,
|
||||||
|
type Rel,
|
||||||
|
} from "@mikro-orm/core";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class FlightSchedule {
|
||||||
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => FlightClass)
|
||||||
|
flight!: Rel<FlightClass>;
|
||||||
|
|
||||||
|
@ManyToOne(() => FlightSchedule, {
|
||||||
|
nullable: true,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
|
next!: Rel<FlightSchedule> | null;
|
||||||
|
|
||||||
|
@Property({
|
||||||
|
type: "timestamp",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
})
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@Property({
|
||||||
|
type: "timestamp",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
onUpdate: () => new Date(),
|
||||||
|
})
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Airline } from "@/database/entities/airline.entity";
|
|||||||
import { Airport } from "@/database/entities/airport.entity";
|
import { Airport } from "@/database/entities/airport.entity";
|
||||||
import { FlightClass } from "@/database/entities/flight-class.entity";
|
import { FlightClass } from "@/database/entities/flight-class.entity";
|
||||||
import {
|
import {
|
||||||
|
Cascade,
|
||||||
Collection,
|
Collection,
|
||||||
Entity,
|
Entity,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
@@ -18,10 +19,6 @@ export class Flight {
|
|||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 220 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Airline)
|
@ManyToOne(() => Airline)
|
||||||
airline!: Rel<Airline>;
|
airline!: Rel<Airline>;
|
||||||
|
|
||||||
@@ -32,10 +29,10 @@ export class Flight {
|
|||||||
departureAirport!: Rel<Airport>;
|
departureAirport!: Rel<Airport>;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100, nullable: true })
|
@Property({ type: "varchar", length: 100, nullable: true })
|
||||||
departureTerminal?: string;
|
departureTerminal!: string | null;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100, nullable: true })
|
@Property({ type: "varchar", length: 100, nullable: true })
|
||||||
departureGate?: string;
|
departureGate!: string | null;
|
||||||
|
|
||||||
@Property({ type: "time" })
|
@Property({ type: "time" })
|
||||||
departureTime!: string;
|
departureTime!: string;
|
||||||
@@ -44,10 +41,10 @@ export class Flight {
|
|||||||
arrivalAirport!: Rel<Airport>;
|
arrivalAirport!: Rel<Airport>;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100, nullable: true })
|
@Property({ type: "varchar", length: 100, nullable: true })
|
||||||
arrivalTerminal?: string;
|
arrivalTerminal!: string | null;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100, nullable: true })
|
@Property({ type: "varchar", length: 100, nullable: true })
|
||||||
arrivalGate?: string;
|
arrivalGate!: string | null;
|
||||||
|
|
||||||
@Property({ type: "integer", unsigned: true })
|
@Property({ type: "integer", unsigned: true })
|
||||||
duration!: number;
|
duration!: number;
|
||||||
@@ -70,6 +67,9 @@ export class Flight {
|
|||||||
|
|
||||||
// Collections
|
// Collections
|
||||||
|
|
||||||
@OneToMany(() => FlightClass, (flightClass) => flightClass.flight)
|
@OneToMany(() => FlightClass, (flightClass) => flightClass.flight, {
|
||||||
|
orphanRemoval: true,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
classes = new Collection<FlightClass>(this);
|
classes = new Collection<FlightClass>(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
import { Hotel } from "@/database/entities/hotel.entity";
|
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
|
||||||
import {
|
|
||||||
Collection,
|
|
||||||
Entity,
|
|
||||||
ManyToMany,
|
|
||||||
PrimaryKey,
|
|
||||||
Property,
|
|
||||||
Unique,
|
|
||||||
} from "@mikro-orm/core";
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class HotelFacility {
|
export class HotelFacility {
|
||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 200 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@@ -35,9 +23,4 @@ export class HotelFacility {
|
|||||||
onUpdate: () => new Date(),
|
onUpdate: () => new Date(),
|
||||||
})
|
})
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
// Collections
|
|
||||||
|
|
||||||
@ManyToMany(() => Hotel, (hotel) => hotel.facilities)
|
|
||||||
hotels = new Collection<HotelFacility>(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Hotel } from "@/database/entities/hotel.entity";
|
import { Hotel } from "@/database/entities/hotel.entity";
|
||||||
import { PackageDetail } from "@/database/entities/package-detail.entity";
|
|
||||||
import {
|
import {
|
||||||
Collection,
|
|
||||||
Entity,
|
Entity,
|
||||||
ManyToMany,
|
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
@@ -36,9 +33,4 @@ export class HotelSchedule {
|
|||||||
onUpdate: () => new Date(),
|
onUpdate: () => new Date(),
|
||||||
})
|
})
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
// Inverse side
|
|
||||||
|
|
||||||
@ManyToMany(() => PackageDetail, (packageDetail) => packageDetail.tourHotels)
|
|
||||||
tourPackageDetails = new Collection<PackageDetail>(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { City } from "@/database/entities/city.entity";
|
|||||||
import { HotelFacility } from "@/database/entities/hotel-facility.entity";
|
import { HotelFacility } from "@/database/entities/hotel-facility.entity";
|
||||||
import { HotelImage } from "@/database/entities/hotel-image.entity";
|
import { HotelImage } from "@/database/entities/hotel-image.entity";
|
||||||
import {
|
import {
|
||||||
|
Cascade,
|
||||||
Collection,
|
Collection,
|
||||||
Entity,
|
Entity,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
Unique,
|
|
||||||
type Rel,
|
type Rel,
|
||||||
} from "@mikro-orm/core";
|
} from "@mikro-orm/core";
|
||||||
|
|
||||||
@@ -18,10 +18,6 @@ export class Hotel {
|
|||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 200 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@@ -31,18 +27,27 @@ export class Hotel {
|
|||||||
@Property({ type: "integer", unsigned: true })
|
@Property({ type: "integer", unsigned: true })
|
||||||
star!: number;
|
star!: number;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 500 })
|
@OneToMany(() => HotelImage, (hotelImage) => hotelImage.hotel, {
|
||||||
|
orphanRemoval: true,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
|
images = new Collection<HotelImage>(this);
|
||||||
|
|
||||||
|
@Property({ type: "varchar", length: 1000 })
|
||||||
googleMapsLink!: string;
|
googleMapsLink!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 500 })
|
@Property({ type: "varchar", length: 1000 })
|
||||||
googleMapsEmbed!: string;
|
googleMapsEmbed!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 500 })
|
@Property({ type: "varchar", length: 1000 })
|
||||||
googleReviewsLink!: string;
|
googleReviewsLink!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 1000 })
|
@Property({ type: "varchar", length: 1000 })
|
||||||
description!: string;
|
description!: string;
|
||||||
|
|
||||||
|
@ManyToMany(() => HotelFacility)
|
||||||
|
facilities = new Collection<HotelFacility>(this);
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
address!: string;
|
address!: string;
|
||||||
|
|
||||||
@@ -50,13 +55,13 @@ export class Hotel {
|
|||||||
landmark!: string;
|
landmark!: string;
|
||||||
|
|
||||||
@Property({ type: "decimal" })
|
@Property({ type: "decimal" })
|
||||||
distanceToLandmark!: string;
|
distanceToLandmark!: number;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
foodType!: string;
|
foodType!: string;
|
||||||
|
|
||||||
@Property({ type: "integer", unsigned: true })
|
@Property({ type: "integer", unsigned: true })
|
||||||
foodAmount!: string;
|
foodAmount!: number;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
foodMenu!: string;
|
foodMenu!: string;
|
||||||
@@ -73,14 +78,4 @@ export class Hotel {
|
|||||||
onUpdate: () => new Date(),
|
onUpdate: () => new Date(),
|
||||||
})
|
})
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
// Collections
|
|
||||||
|
|
||||||
@OneToMany(() => HotelImage, (hotelImage) => hotelImage.hotel)
|
|
||||||
images = new Collection<HotelImage>(this);
|
|
||||||
|
|
||||||
@ManyToMany(() => HotelFacility, (hotelFacility) => hotelFacility.hotels, {
|
|
||||||
owner: true,
|
|
||||||
})
|
|
||||||
facilities = new Collection<HotelFacility>(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { FlightClass } from "@/database/entities/flight-class.entity";
|
import { FlightSchedule } from "@/database/entities/flight-schedule.entity";
|
||||||
import { HotelSchedule } from "@/database/entities/hotel-schedule.entity";
|
import { HotelSchedule } from "@/database/entities/hotel-schedule.entity";
|
||||||
|
import { PackageItinerary } from "@/database/entities/package-itinerary.entity";
|
||||||
import { Package } from "@/database/entities/package.entity";
|
import { Package } from "@/database/entities/package.entity";
|
||||||
import { TransportationClass } from "@/database/entities/transportation-class.entity";
|
import { TransportationClass } from "@/database/entities/transportation-class.entity";
|
||||||
import {
|
import {
|
||||||
|
Cascade,
|
||||||
Collection,
|
Collection,
|
||||||
Entity,
|
Entity,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
@@ -19,60 +21,42 @@ export class PackageDetail {
|
|||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 200 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Package)
|
@ManyToOne(() => Package)
|
||||||
package!: Rel<Package>;
|
package!: Rel<Package>;
|
||||||
|
|
||||||
@Property({ type: "date" })
|
@Property({ type: "date" })
|
||||||
departureDate!: Date;
|
departureDate!: Date;
|
||||||
|
|
||||||
@ManyToMany(
|
@ManyToOne(() => FlightSchedule, {
|
||||||
() => FlightClass,
|
cascade: [Cascade.REMOVE],
|
||||||
(flightClass) => flightClass.tourPackageDetails,
|
nullable: true,
|
||||||
{
|
})
|
||||||
owner: true,
|
tourFlight!: Rel<FlightSchedule> | null;
|
||||||
fixedOrder: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
tourFlightClasses = new Collection<FlightClass>(this);
|
|
||||||
|
|
||||||
@ManyToMany(
|
@ManyToOne(() => FlightSchedule, {
|
||||||
() => FlightClass,
|
cascade: [Cascade.REMOVE],
|
||||||
(flightClass) => flightClass.outboundPackageDetails,
|
})
|
||||||
{
|
outboundFlight!: Rel<FlightSchedule>;
|
||||||
owner: true,
|
|
||||||
fixedOrder: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
outboundFlightClasses = new Collection<FlightClass>(this);
|
|
||||||
|
|
||||||
@ManyToMany(
|
@ManyToOne(() => FlightSchedule, {
|
||||||
() => FlightClass,
|
cascade: [Cascade.REMOVE],
|
||||||
(flightClass) => flightClass.inboundPackageDetails,
|
})
|
||||||
{
|
inboundFlight!: Rel<FlightSchedule>;
|
||||||
owner: true,
|
|
||||||
fixedOrder: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
inboundFlightClasses = new Collection<FlightClass>(this);
|
|
||||||
|
|
||||||
@ManyToMany(
|
@ManyToMany(() => HotelSchedule, undefined, {
|
||||||
() => HotelSchedule,
|
owner: true,
|
||||||
(hotelSchedule) => hotelSchedule.tourPackageDetails,
|
cascade: [Cascade.REMOVE],
|
||||||
{
|
})
|
||||||
owner: true,
|
|
||||||
fixedOrder: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
tourHotels = new Collection<HotelSchedule>(this);
|
tourHotels = new Collection<HotelSchedule>(this);
|
||||||
|
|
||||||
@ManyToOne(() => HotelSchedule)
|
@ManyToOne(() => HotelSchedule, {
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
makkahHotel!: Rel<HotelSchedule>;
|
makkahHotel!: Rel<HotelSchedule>;
|
||||||
|
|
||||||
@ManyToOne(() => HotelSchedule)
|
@ManyToOne(() => HotelSchedule, {
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
madinahHotel!: Rel<HotelSchedule>;
|
madinahHotel!: Rel<HotelSchedule>;
|
||||||
|
|
||||||
@ManyToOne(() => TransportationClass)
|
@ManyToOne(() => TransportationClass)
|
||||||
@@ -88,7 +72,12 @@ export class PackageDetail {
|
|||||||
doublePrice!: number;
|
doublePrice!: number;
|
||||||
|
|
||||||
@Property({ type: "decimal", nullable: true, unsigned: true })
|
@Property({ type: "decimal", nullable: true, unsigned: true })
|
||||||
infantPrice?: number;
|
infantPrice!: number | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => PackageItinerary, {
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
|
itinerary!: Rel<PackageItinerary>;
|
||||||
|
|
||||||
@Property({
|
@Property({
|
||||||
type: "timestamp",
|
type: "timestamp",
|
||||||
|
|||||||
52
src/database/entities/package-itinerary-day.entity.ts
Normal file
52
src/database/entities/package-itinerary-day.entity.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { PackageItineraryWidget } from "@/database/entities/package-itinerary-widget.entity";
|
||||||
|
import {
|
||||||
|
Cascade,
|
||||||
|
Collection,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
PrimaryKey,
|
||||||
|
Property,
|
||||||
|
type Rel,
|
||||||
|
} from "@mikro-orm/core";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class PackageItineraryDay {
|
||||||
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Property({ type: "varchar", length: 100 })
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@Property({ type: "varchar", length: 1000 })
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => PackageItineraryWidget,
|
||||||
|
(widget) => widget.packageItineraryDay,
|
||||||
|
{
|
||||||
|
orphanRemoval: true,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
widgets = new Collection<PackageItineraryWidget>(this);
|
||||||
|
|
||||||
|
@ManyToOne(() => PackageItineraryDay, {
|
||||||
|
nullable: true,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
|
next!: Rel<PackageItineraryDay> | null;
|
||||||
|
|
||||||
|
@Property({
|
||||||
|
type: "timestamp",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
})
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@Property({
|
||||||
|
type: "timestamp",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
onUpdate: () => new Date(),
|
||||||
|
})
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
||||||
34
src/database/entities/package-itinerary-image.entity.ts
Normal file
34
src/database/entities/package-itinerary-image.entity.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { PackageItinerary } from "@/database/entities/package-itinerary.entity";
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryKey,
|
||||||
|
Property,
|
||||||
|
type Rel,
|
||||||
|
} from "@mikro-orm/core";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class PackageItineraryImage {
|
||||||
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => PackageItinerary)
|
||||||
|
packageItinerary!: Rel<PackageItinerary>;
|
||||||
|
|
||||||
|
@Property({ type: "varchar", length: 100 })
|
||||||
|
src!: string;
|
||||||
|
|
||||||
|
@Property({ type: "varchar", length: 100 })
|
||||||
|
@Property({
|
||||||
|
type: "timestamp",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
})
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@Property({
|
||||||
|
type: "timestamp",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
onUpdate: () => new Date(),
|
||||||
|
})
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
||||||
63
src/database/entities/package-itinerary-widget.entity.ts
Normal file
63
src/database/entities/package-itinerary-widget.entity.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Hotel } from "@/database/entities/hotel.entity";
|
||||||
|
import { PackageItineraryDay } from "@/database/entities/package-itinerary-day.entity";
|
||||||
|
import { PackageItineraryWidgetType } from "@/database/enums/package-itinerary-widget-type.enum";
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
Enum,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryKey,
|
||||||
|
Property,
|
||||||
|
type Rel,
|
||||||
|
} from "@mikro-orm/core";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
abstract: true,
|
||||||
|
discriminatorColumn: "type",
|
||||||
|
})
|
||||||
|
export abstract class PackageItineraryWidget {
|
||||||
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => PackageItineraryDay)
|
||||||
|
packageItineraryDay!: Rel<PackageItineraryDay>;
|
||||||
|
|
||||||
|
@Enum(() => PackageItineraryWidgetType)
|
||||||
|
type!: PackageItineraryWidgetType;
|
||||||
|
|
||||||
|
@Property({
|
||||||
|
type: "timestamp",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
})
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@Property({
|
||||||
|
type: "timestamp",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
onUpdate: () => new Date(),
|
||||||
|
})
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ discriminatorValue: "transport" })
|
||||||
|
export class PackageItineraryWidgetTransport extends PackageItineraryWidget {
|
||||||
|
@Property({ type: "varchar", length: 100 })
|
||||||
|
transportation!: string;
|
||||||
|
|
||||||
|
@Property({ type: "varchar", length: 100 })
|
||||||
|
from!: string;
|
||||||
|
|
||||||
|
@Property({ type: "varchar", length: 100 })
|
||||||
|
to!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ discriminatorValue: "hotel" })
|
||||||
|
export class PackageItineraryWidgetHotel extends PackageItineraryWidget {
|
||||||
|
@ManyToOne(() => Hotel)
|
||||||
|
hotel!: Rel<Hotel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ discriminatorValue: "information" })
|
||||||
|
export class PackageItineraryWidgetInformation extends PackageItineraryWidget {
|
||||||
|
@Property({ type: "varchar", length: 1000 })
|
||||||
|
description!: string;
|
||||||
|
}
|
||||||
55
src/database/entities/package-itinerary.entity.ts
Normal file
55
src/database/entities/package-itinerary.entity.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { PackageItineraryDay } from "@/database/entities/package-itinerary-day.entity";
|
||||||
|
import { PackageItineraryImage } from "@/database/entities/package-itinerary-image.entity";
|
||||||
|
import {
|
||||||
|
Cascade,
|
||||||
|
Collection,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
PrimaryKey,
|
||||||
|
Property,
|
||||||
|
type Rel,
|
||||||
|
} from "@mikro-orm/core";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class PackageItinerary {
|
||||||
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Property({ type: "varchar", length: 100 })
|
||||||
|
location!: string;
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => PackageItineraryImage,
|
||||||
|
(packageItineraryImage) => packageItineraryImage.packageItinerary,
|
||||||
|
{
|
||||||
|
orphanRemoval: true,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
images = new Collection<PackageItineraryImage>(this);
|
||||||
|
|
||||||
|
@ManyToOne(() => PackageItineraryDay, {
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
|
day!: Rel<PackageItineraryDay>;
|
||||||
|
|
||||||
|
@ManyToOne(() => PackageItinerary, {
|
||||||
|
nullable: true,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
|
next!: Rel<PackageItinerary> | null;
|
||||||
|
|
||||||
|
@Property({
|
||||||
|
type: "timestamp",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
})
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@Property({
|
||||||
|
type: "timestamp",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
onUpdate: () => new Date(),
|
||||||
|
})
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { PackageDetail } from "@/database/entities/package-detail.entity";
|
|||||||
import { PackageClass } from "@/database/enums/package-class.enum";
|
import { PackageClass } from "@/database/enums/package-class.enum";
|
||||||
import { PackageType } from "@/database/enums/package-type.enum";
|
import { PackageType } from "@/database/enums/package-type.enum";
|
||||||
import {
|
import {
|
||||||
|
Cascade,
|
||||||
Collection,
|
Collection,
|
||||||
Entity,
|
Entity,
|
||||||
Enum,
|
Enum,
|
||||||
@@ -16,10 +17,6 @@ export class Package {
|
|||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 200 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@@ -51,6 +48,9 @@ export class Package {
|
|||||||
|
|
||||||
// Collections
|
// Collections
|
||||||
|
|
||||||
@OneToMany(() => PackageDetail, (packageDetail) => packageDetail.package)
|
@OneToMany(() => PackageDetail, (packageDetail) => packageDetail.package, {
|
||||||
|
orphanRemoval: true,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
details = new Collection<PackageDetail>(this);
|
details = new Collection<PackageDetail>(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PackageDetail } from "@/database/entities/package-detail.entity";
|
import { TransportationImage } from "@/database/entities/transportation-image.entity";
|
||||||
import { TransportationClassImage } from "@/database/entities/transportation-class-image.entity";
|
|
||||||
import { Transportation } from "@/database/entities/transportation.entity";
|
import { Transportation } from "@/database/entities/transportation.entity";
|
||||||
import {
|
import {
|
||||||
|
Cascade,
|
||||||
Collection,
|
Collection,
|
||||||
Entity,
|
Entity,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
@@ -18,10 +18,6 @@ export class TransportationClass {
|
|||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 200 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Transportation)
|
@ManyToOne(() => Transportation)
|
||||||
transportation!: Rel<Transportation>;
|
transportation!: Rel<Transportation>;
|
||||||
|
|
||||||
@@ -31,6 +27,16 @@ export class TransportationClass {
|
|||||||
@Property({ type: "integer", unsigned: true })
|
@Property({ type: "integer", unsigned: true })
|
||||||
totalSeats!: number;
|
totalSeats!: number;
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => TransportationImage,
|
||||||
|
(transportationClassImage) => transportationClassImage.transportation,
|
||||||
|
{
|
||||||
|
orphanRemoval: true,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
images = new Collection<TransportationImage>(this);
|
||||||
|
|
||||||
@Property({
|
@Property({
|
||||||
type: "timestamp",
|
type: "timestamp",
|
||||||
onCreate: () => new Date(),
|
onCreate: () => new Date(),
|
||||||
@@ -43,18 +49,4 @@ export class TransportationClass {
|
|||||||
onUpdate: () => new Date(),
|
onUpdate: () => new Date(),
|
||||||
})
|
})
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
// Collections
|
|
||||||
|
|
||||||
@OneToMany(
|
|
||||||
() => TransportationClassImage,
|
|
||||||
(transportationClassImage) => transportationClassImage.transportationClass,
|
|
||||||
)
|
|
||||||
images = new Collection<TransportationClass>(this);
|
|
||||||
|
|
||||||
@OneToMany(
|
|
||||||
() => PackageDetail,
|
|
||||||
(packageDetail) => packageDetail.transportation,
|
|
||||||
)
|
|
||||||
packageDetails = new Collection<PackageDetail>(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import {
|
|||||||
} from "@mikro-orm/core";
|
} from "@mikro-orm/core";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class TransportationClassImage {
|
export class TransportationImage {
|
||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ManyToOne(() => TransportationClass)
|
@ManyToOne(() => TransportationClass)
|
||||||
transportationClass!: Rel<TransportationClass>;
|
transportation!: Rel<TransportationClass>;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
src!: string;
|
src!: string;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { TransportationClass } from "@/database/entities/transportation-class.entity";
|
import { TransportationClass } from "@/database/entities/transportation-class.entity";
|
||||||
import {
|
import {
|
||||||
|
Cascade,
|
||||||
Collection,
|
Collection,
|
||||||
Entity,
|
Entity,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
Unique,
|
|
||||||
} from "@mikro-orm/core";
|
} from "@mikro-orm/core";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@@ -13,10 +13,6 @@ export class Transportation {
|
|||||||
@PrimaryKey({ type: "varchar", length: 30 })
|
@PrimaryKey({ type: "varchar", length: 30 })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
|
||||||
@Unique()
|
|
||||||
slug!: string;
|
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 100 })
|
@Property({ type: "varchar", length: 100 })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@@ -41,6 +37,10 @@ export class Transportation {
|
|||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => TransportationClass,
|
() => TransportationClass,
|
||||||
(transportationClass) => transportationClass.transportation,
|
(transportationClass) => transportationClass.transportation,
|
||||||
|
{
|
||||||
|
orphanRemoval: true,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
transportationClasses = new Collection<TransportationClass>(this);
|
classes = new Collection<TransportationClass>(this);
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/database/enums/package-itinerary-widget-type.enum.ts
Normal file
5
src/database/enums/package-itinerary-widget-type.enum.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum PackageItineraryWidgetType {
|
||||||
|
transport = "transport",
|
||||||
|
hotel = "hotel",
|
||||||
|
information = "information",
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
226
src/database/migrations/Migration20251110080126.ts
Normal file
226
src/database/migrations/Migration20251110080126.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations";
|
||||||
|
|
||||||
|
export class Migration20251110080126 extends Migration {
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`create table "flight_schedule" ("id" varchar(30) not null, "flight_id" varchar(30) not null, "next_id" varchar(30) null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "flight_schedule_pkey" primary key ("id"));`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`create table "package_itinerary_day" ("id" varchar(30) not null, "title" varchar(100) not null, "description" varchar(1000) not null, "next_id" varchar(30) null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "package_itinerary_day_pkey" primary key ("id"));`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`create table "package_itinerary" ("id" varchar(30) not null, "location" varchar(100) not null, "day_id" varchar(30) not null, "next_id" varchar(30) null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "package_itinerary_pkey" primary key ("id"));`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`create table "package_itinerary_image" ("id" varchar(30) not null, "package_itinerary_id" varchar(30) not null, "src" varchar(100) not null, "created_at" varchar(100) not null, "updated_at" timestamptz not null, constraint "package_itinerary_image_pkey" primary key ("id"));`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`create table "package_itinerary_widget" ("id" varchar(30) not null, "package_itinerary_day_id" varchar(30) not null, "type" text check ("type" in ('transport', 'hotel', 'information')) not null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "hotel_id" varchar(30) null, "description" varchar(1000) null, "transportation" varchar(100) null, "from" varchar(100) null, "to" varchar(100) null, constraint "package_itinerary_widget_pkey" primary key ("id"));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`create index "package_itinerary_widget_type_index" on "package_itinerary_widget" ("type");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`create table "transportation_image" ("id" varchar(30) not null, "transportation_id" varchar(30) not null, "src" varchar(100) not null, "created_at" varchar(100) not null, "updated_at" timestamptz not null, constraint "transportation_image_pkey" primary key ("id"));`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight_schedule" add constraint "flight_schedule_flight_id_foreign" foreign key ("flight_id") references "flight_class" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight_schedule" add constraint "flight_schedule_next_id_foreign" foreign key ("next_id") references "flight_schedule" ("id") on update cascade on delete set null;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_day" add constraint "package_itinerary_day_next_id_foreign" foreign key ("next_id") references "package_itinerary_day" ("id") on update cascade on delete set null;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" add constraint "package_itinerary_day_id_foreign" foreign key ("day_id") references "package_itinerary_day" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" add constraint "package_itinerary_next_id_foreign" foreign key ("next_id") references "package_itinerary" ("id") on update cascade on delete set null;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_image" add constraint "package_itinerary_image_package_itinerary_id_foreign" foreign key ("package_itinerary_id") references "package_itinerary" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_widget" add constraint "package_itinerary_widget_package_itinerary_day_id_foreign" foreign key ("package_itinerary_day_id") references "package_itinerary_day" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_widget" add constraint "package_itinerary_widget_hotel_id_foreign" foreign key ("hotel_id") references "hotel" ("id") on update cascade on delete set null;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_image" add constraint "transportation_image_transportation_id_foreign" foreign key ("transportation_id") references "transportation_class" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`drop table if exists "package_detail_tour_flight_classes" cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`drop table if exists "package_detail_outbound_flight_classes" cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`drop table if exists "package_detail_inbound_flight_classes" cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(`drop table if exists "transportation_class_image" cascade;`);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "airline" alter column "skytrax_rating" type int using ("skytrax_rating"::int);`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "airline" alter column "skytrax_rating" set not null;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add column "tour_flight_id" varchar(30) not null, add column "outbound_flight_id" varchar(30) not null, add column "inbound_flight_id" varchar(30) not null, add column "package_itinerary_id" varchar(30) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_tour_flight_id_foreign" foreign key ("tour_flight_id") references "flight_class" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_outbound_flight_id_foreign" foreign key ("outbound_flight_id") references "flight_class" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_inbound_flight_id_foreign" foreign key ("inbound_flight_id") references "flight_class" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_package_itinerary_id_foreign" foreign key ("package_itinerary_id") references "package_itinerary" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_tour_hotels" drop constraint "package_detail_tour_hotels_pkey";`,
|
||||||
|
);
|
||||||
|
this.addSql(`alter table "package_detail_tour_hotels" drop column "id";`);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_tour_hotels" add constraint "package_detail_tour_hotels_pkey" primary key ("package_detail_id", "hotel_schedule_id");`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight_schedule" drop constraint "flight_schedule_next_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_day" drop constraint "package_itinerary_day_next_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" drop constraint "package_itinerary_day_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_widget" drop constraint "package_itinerary_widget_package_itinerary_day_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" drop constraint "package_itinerary_next_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_image" drop constraint "package_itinerary_image_package_itinerary_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_package_itinerary_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`create table "package_detail_tour_flight_classes" ("id" serial primary key, "package_detail_id" varchar(30) not null, "flight_class_id" varchar(30) not null);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`create table "package_detail_outbound_flight_classes" ("id" serial primary key, "package_detail_id" varchar(30) not null, "flight_class_id" varchar(30) not null);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`create table "package_detail_inbound_flight_classes" ("id" serial primary key, "package_detail_id" varchar(30) not null, "flight_class_id" varchar(30) not null);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`create table "transportation_class_image" ("id" varchar(30) not null, "transportation_class_id" varchar(30) not null, "src" varchar(100) not null, "created_at" varchar(100) not null, "updated_at" timestamptz not null, constraint "transportation_class_image_pkey" primary key ("id"));`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_tour_flight_classes" add constraint "package_detail_tour_flight_classes_package_detail_id_foreign" foreign key ("package_detail_id") references "package_detail" ("id") on update cascade on delete cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_tour_flight_classes" add constraint "package_detail_tour_flight_classes_flight_class_id_foreign" foreign key ("flight_class_id") references "flight_class" ("id") on update cascade on delete cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_outbound_flight_classes" add constraint "package_detail_outbound_flight_classes_package_d_8b70d_foreign" foreign key ("package_detail_id") references "package_detail" ("id") on update cascade on delete cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_outbound_flight_classes" add constraint "package_detail_outbound_flight_classes_flight_class_id_foreign" foreign key ("flight_class_id") references "flight_class" ("id") on update cascade on delete cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_inbound_flight_classes" add constraint "package_detail_inbound_flight_classes_package_detail_id_foreign" foreign key ("package_detail_id") references "package_detail" ("id") on update cascade on delete cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_inbound_flight_classes" add constraint "package_detail_inbound_flight_classes_flight_class_id_foreign" foreign key ("flight_class_id") references "flight_class" ("id") on update cascade on delete cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_class_image" add constraint "transportation_class_image_transportation_class_id_foreign" foreign key ("transportation_class_id") references "transportation_class" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(`drop table if exists "flight_schedule" cascade;`);
|
||||||
|
|
||||||
|
this.addSql(`drop table if exists "package_itinerary_day" cascade;`);
|
||||||
|
|
||||||
|
this.addSql(`drop table if exists "package_itinerary" cascade;`);
|
||||||
|
|
||||||
|
this.addSql(`drop table if exists "package_itinerary_image" cascade;`);
|
||||||
|
|
||||||
|
this.addSql(`drop table if exists "package_itinerary_widget" cascade;`);
|
||||||
|
|
||||||
|
this.addSql(`drop table if exists "transportation_image" cascade;`);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_tour_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_outbound_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_inbound_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "airline" alter column "skytrax_rating" type int using ("skytrax_rating"::int);`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "airline" alter column "skytrax_rating" drop not null;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop column "tour_flight_id", drop column "outbound_flight_id", drop column "inbound_flight_id", drop column "package_itinerary_id";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_tour_hotels" drop constraint "package_detail_tour_hotels_pkey";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_tour_hotels" add column "id" serial not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail_tour_hotels" add constraint "package_detail_tour_hotels_pkey" primary key ("id");`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/database/migrations/Migration20251111010042.ts
Normal file
133
src/database/migrations/Migration20251111010042.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations";
|
||||||
|
|
||||||
|
export class Migration20251111010042 extends Migration {
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(`alter table "airline" drop constraint "airline_slug_unique";`);
|
||||||
|
this.addSql(`alter table "airline" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "country" drop constraint "country_slug_unique";`);
|
||||||
|
this.addSql(`alter table "country" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "city" drop constraint "city_slug_unique";`);
|
||||||
|
this.addSql(`alter table "city" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "airport" drop constraint "airport_slug_unique";`);
|
||||||
|
this.addSql(`alter table "airport" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "flight" drop constraint "flight_slug_unique";`);
|
||||||
|
this.addSql(`alter table "flight" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight_class" drop constraint "flight_class_slug_unique";`,
|
||||||
|
);
|
||||||
|
this.addSql(`alter table "flight_class" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "hotel" drop constraint "hotel_slug_unique";`);
|
||||||
|
this.addSql(`alter table "hotel" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_facility" drop constraint "hotel_facility_slug_unique";`,
|
||||||
|
);
|
||||||
|
this.addSql(`alter table "hotel_facility" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "package" drop constraint "package_slug_unique";`);
|
||||||
|
this.addSql(`alter table "package" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation" drop constraint "transportation_slug_unique";`,
|
||||||
|
);
|
||||||
|
this.addSql(`alter table "transportation" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_class" drop constraint "transportation_class_slug_unique";`,
|
||||||
|
);
|
||||||
|
this.addSql(`alter table "transportation_class" drop column "slug";`);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_slug_unique";`,
|
||||||
|
);
|
||||||
|
this.addSql(`alter table "package_detail" drop column "slug";`);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "airline" add column "slug" varchar(200) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "airline" add constraint "airline_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "country" add column "slug" varchar(200) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "country" add constraint "country_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(`alter table "city" add column "slug" varchar(200) not null;`);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "city" add constraint "city_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "airport" add column "slug" varchar(200) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "airport" add constraint "airport_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight" add column "slug" varchar(220) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight" add constraint "flight_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight_class" add column "slug" varchar(200) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight_class" add constraint "flight_class_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(`alter table "hotel" add column "slug" varchar(200) not null;`);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel" add constraint "hotel_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_facility" add column "slug" varchar(200) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_facility" add constraint "hotel_facility_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package" add column "slug" varchar(200) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package" add constraint "package_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation" add column "slug" varchar(100) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation" add constraint "transportation_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_class" add column "slug" varchar(200) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_class" add constraint "transportation_class_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add column "slug" varchar(200) not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_slug_unique" unique ("slug");`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/database/migrations/Migration20251112104244.ts
Normal file
27
src/database/migrations/Migration20251112104244.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations";
|
||||||
|
|
||||||
|
export class Migration20251112104244 extends Migration {
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel" alter column "google_maps_link" type varchar(1000) using ("google_maps_link"::varchar(1000));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel" alter column "google_maps_embed" type varchar(1000) using ("google_maps_embed"::varchar(1000));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel" alter column "google_reviews_link" type varchar(1000) using ("google_reviews_link"::varchar(1000));`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel" alter column "google_maps_link" type varchar(500) using ("google_maps_link"::varchar(500));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel" alter column "google_maps_embed" type varchar(500) using ("google_maps_embed"::varchar(500));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel" alter column "google_reviews_link" type varchar(500) using ("google_reviews_link"::varchar(500));`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/database/migrations/Migration20251112105017.ts
Normal file
35
src/database/migrations/Migration20251112105017.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations";
|
||||||
|
|
||||||
|
export class Migration20251112105017 extends Migration {
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" drop constraint "hotel_image_hotel_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" alter column "hotel_id" type varchar(30) using ("hotel_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" alter column "hotel_id" drop not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" add constraint "hotel_image_hotel_id_foreign" foreign key ("hotel_id") references "hotel" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" drop constraint "hotel_image_hotel_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" alter column "hotel_id" type varchar(30) using ("hotel_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" alter column "hotel_id" set not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" add constraint "hotel_image_hotel_id_foreign" foreign key ("hotel_id") references "hotel" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/database/migrations/Migration20251112105413.ts
Normal file
21
src/database/migrations/Migration20251112105413.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
|
export class Migration20251112105413 extends Migration {
|
||||||
|
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(`alter table "transportation_image" drop constraint "transportation_image_transportation_id_foreign";`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "transportation_image" alter column "transportation_id" type varchar(30) using ("transportation_id"::varchar(30));`);
|
||||||
|
this.addSql(`alter table "transportation_image" alter column "transportation_id" drop not null;`);
|
||||||
|
this.addSql(`alter table "transportation_image" add constraint "transportation_image_transportation_id_foreign" foreign key ("transportation_id") references "transportation_class" ("id") on delete cascade;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(`alter table "transportation_image" drop constraint "transportation_image_transportation_id_foreign";`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "transportation_image" alter column "transportation_id" type varchar(30) using ("transportation_id"::varchar(30));`);
|
||||||
|
this.addSql(`alter table "transportation_image" alter column "transportation_id" set not null;`);
|
||||||
|
this.addSql(`alter table "transportation_image" add constraint "transportation_image_transportation_id_foreign" foreign key ("transportation_id") references "transportation_class" ("id") on update cascade;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
47
src/database/migrations/Migration20251112134446.ts
Normal file
47
src/database/migrations/Migration20251112134446.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations";
|
||||||
|
|
||||||
|
export class Migration20251112134446 extends Migration {
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_tour_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_outbound_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_inbound_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_tour_flight_id_foreign" foreign key ("tour_flight_id") references "flight_schedule" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_outbound_flight_id_foreign" foreign key ("outbound_flight_id") references "flight_schedule" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_inbound_flight_id_foreign" foreign key ("inbound_flight_id") references "flight_schedule" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_tour_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_outbound_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_inbound_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_tour_flight_id_foreign" foreign key ("tour_flight_id") references "flight_class" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_outbound_flight_id_foreign" foreign key ("outbound_flight_id") references "flight_class" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_inbound_flight_id_foreign" foreign key ("inbound_flight_id") references "flight_class" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/database/migrations/Migration20251112140208.ts
Normal file
29
src/database/migrations/Migration20251112140208.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations";
|
||||||
|
|
||||||
|
export class Migration20251112140208 extends Migration {
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_package_itinerary_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" rename column "package_itinerary_id" to "itinerary_id";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_itinerary_id_foreign" foreign key ("itinerary_id") references "package_itinerary" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_itinerary_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" rename column "itinerary_id" to "package_itinerary_id";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_package_itinerary_id_foreign" foreign key ("package_itinerary_id") references "package_itinerary" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
283
src/database/migrations/Migration20251113230544.ts
Normal file
283
src/database/migrations/Migration20251113230544.ts
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations";
|
||||||
|
|
||||||
|
export class Migration20251113230544 extends Migration {
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight_schedule" drop constraint "flight_schedule_next_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" drop constraint "hotel_image_hotel_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_day" drop constraint "package_itinerary_day_next_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" drop constraint "package_itinerary_day_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" drop constraint "package_itinerary_next_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_tour_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_outbound_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_inbound_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_makkah_hotel_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_madinah_hotel_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_itinerary_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_image" drop constraint "transportation_image_transportation_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight_schedule" add constraint "flight_schedule_next_id_foreign" foreign key ("next_id") references "flight_schedule" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" alter column "hotel_id" type varchar(30) using ("hotel_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" alter column "hotel_id" set not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" add constraint "hotel_image_hotel_id_foreign" foreign key ("hotel_id") references "hotel" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_day" add constraint "package_itinerary_day_next_id_foreign" foreign key ("next_id") references "package_itinerary_day" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" alter column "day_id" type varchar(30) using ("day_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" alter column "day_id" drop not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" add constraint "package_itinerary_day_id_foreign" foreign key ("day_id") references "package_itinerary_day" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" add constraint "package_itinerary_next_id_foreign" foreign key ("next_id") references "package_itinerary" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "tour_flight_id" type varchar(30) using ("tour_flight_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "tour_flight_id" drop not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "outbound_flight_id" type varchar(30) using ("outbound_flight_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "outbound_flight_id" drop not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "inbound_flight_id" type varchar(30) using ("inbound_flight_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "inbound_flight_id" drop not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "makkah_hotel_id" type varchar(30) using ("makkah_hotel_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "makkah_hotel_id" drop not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "madinah_hotel_id" type varchar(30) using ("madinah_hotel_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "madinah_hotel_id" drop not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "itinerary_id" type varchar(30) using ("itinerary_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "itinerary_id" drop not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_tour_flight_id_foreign" foreign key ("tour_flight_id") references "flight_schedule" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_outbound_flight_id_foreign" foreign key ("outbound_flight_id") references "flight_schedule" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_inbound_flight_id_foreign" foreign key ("inbound_flight_id") references "flight_schedule" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_makkah_hotel_id_foreign" foreign key ("makkah_hotel_id") references "hotel_schedule" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_madinah_hotel_id_foreign" foreign key ("madinah_hotel_id") references "hotel_schedule" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_itinerary_id_foreign" foreign key ("itinerary_id") references "package_itinerary" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_image" alter column "transportation_id" type varchar(30) using ("transportation_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_image" alter column "transportation_id" set not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_image" add constraint "transportation_image_transportation_id_foreign" foreign key ("transportation_id") references "transportation_class" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight_schedule" drop constraint "flight_schedule_next_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" drop constraint "hotel_image_hotel_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_day" drop constraint "package_itinerary_day_next_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" drop constraint "package_itinerary_day_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" drop constraint "package_itinerary_next_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_tour_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_outbound_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_inbound_flight_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_makkah_hotel_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_madinah_hotel_id_foreign";`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" drop constraint "package_detail_itinerary_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_image" drop constraint "transportation_image_transportation_id_foreign";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "flight_schedule" add constraint "flight_schedule_next_id_foreign" foreign key ("next_id") references "flight_schedule" ("id") on update cascade on delete set null;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" alter column "hotel_id" type varchar(30) using ("hotel_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" alter column "hotel_id" drop not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "hotel_image" add constraint "hotel_image_hotel_id_foreign" foreign key ("hotel_id") references "hotel" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary_day" add constraint "package_itinerary_day_next_id_foreign" foreign key ("next_id") references "package_itinerary_day" ("id") on update cascade on delete set null;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" alter column "day_id" type varchar(30) using ("day_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" alter column "day_id" set not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" add constraint "package_itinerary_day_id_foreign" foreign key ("day_id") references "package_itinerary_day" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_itinerary" add constraint "package_itinerary_next_id_foreign" foreign key ("next_id") references "package_itinerary" ("id") on update cascade on delete set null;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "tour_flight_id" type varchar(30) using ("tour_flight_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "tour_flight_id" set not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "outbound_flight_id" type varchar(30) using ("outbound_flight_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "outbound_flight_id" set not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "inbound_flight_id" type varchar(30) using ("inbound_flight_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "inbound_flight_id" set not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "makkah_hotel_id" type varchar(30) using ("makkah_hotel_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "makkah_hotel_id" set not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "madinah_hotel_id" type varchar(30) using ("madinah_hotel_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "madinah_hotel_id" set not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "itinerary_id" type varchar(30) using ("itinerary_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" alter column "itinerary_id" set not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_tour_flight_id_foreign" foreign key ("tour_flight_id") references "flight_schedule" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_outbound_flight_id_foreign" foreign key ("outbound_flight_id") references "flight_schedule" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_inbound_flight_id_foreign" foreign key ("inbound_flight_id") references "flight_schedule" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_makkah_hotel_id_foreign" foreign key ("makkah_hotel_id") references "hotel_schedule" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_madinah_hotel_id_foreign" foreign key ("madinah_hotel_id") references "hotel_schedule" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "package_detail" add constraint "package_detail_itinerary_id_foreign" foreign key ("itinerary_id") references "package_itinerary" ("id") on update cascade;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_image" alter column "transportation_id" type varchar(30) using ("transportation_id"::varchar(30));`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_image" alter column "transportation_id" drop not null;`,
|
||||||
|
);
|
||||||
|
this.addSql(
|
||||||
|
`alter table "transportation_image" add constraint "transportation_image_transportation_id_foreign" foreign key ("transportation_id") references "transportation_class" ("id") on delete cascade;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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({
|
export const airlineParamsSchema = z.object({
|
||||||
slug: z
|
id: z
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(200, "Max 200 characters."),
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import type {
|
import type {
|
||||||
airlineQuerySchema,
|
airlineParamsSchema,
|
||||||
airlineRequestSchema,
|
airlineRequestSchema,
|
||||||
} from "@/modules/airline/airline.schemas";
|
} from "@/modules/airline/airline.schemas";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
export type AirlineRequest = z.infer<typeof airlineRequestSchema>;
|
export type AirlineRequest = z.infer<typeof airlineRequestSchema>;
|
||||||
|
|
||||||
export type AirlineQuery = z.infer<typeof airlineQuerySchema>;
|
export type AirlineParams = z.infer<typeof airlineParamsSchema>;
|
||||||
|
|
||||||
export type AirlineResponse = {
|
export type AirlineResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
logo: 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.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(10, "Max 10 characters."),
|
.max(10, "Max 10 characters."),
|
||||||
city_slug: z
|
city_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const airportQuerySchema = z.object({
|
export const airportParamsSchema = z.object({
|
||||||
slug: z
|
id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
airportQuerySchema,
|
airportParamsSchema,
|
||||||
airportRequestSchema,
|
airportRequestSchema,
|
||||||
} from "@/modules/airport/airport.schemas";
|
} from "@/modules/airport/airport.schemas";
|
||||||
import type { CityResponse } from "@/modules/city/city.types";
|
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 AirportRequest = z.infer<typeof airportRequestSchema>;
|
||||||
|
|
||||||
export type AirportQuery = z.infer<typeof airportQuerySchema>;
|
export type AirportParams = z.infer<typeof airportParamsSchema>;
|
||||||
|
|
||||||
export type AirportResponse = {
|
export type AirportResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
city: CityResponse;
|
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.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(100, "Max 100 characters."),
|
.max(100, "Max 100 characters."),
|
||||||
country_slug: z
|
country_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const cityQuerySchema = z.object({
|
export const cityParamsSchema = z.object({
|
||||||
slug: z
|
id: z
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(200, "Max 200 characters."),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
cityQuerySchema,
|
cityParamsSchema,
|
||||||
cityRequestSchema,
|
cityRequestSchema,
|
||||||
} from "@/modules/city/city.schemas";
|
} from "@/modules/city/city.schemas";
|
||||||
import type { CountryResponse } from "@/modules/country/country.types";
|
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 CityRequest = z.infer<typeof cityRequestSchema>;
|
||||||
|
|
||||||
export type CityQuery = z.infer<typeof cityQuerySchema>;
|
export type CityParams = z.infer<typeof cityParamsSchema>;
|
||||||
|
|
||||||
export type CityResponse = {
|
export type CityResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
country: CountryResponse;
|
country: CountryResponse;
|
||||||
created_at: Date;
|
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."),
|
.max(100, "Max 100 characters."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const countryQuerySchema = z.object({
|
export const countryParamsSchema = z.object({
|
||||||
slug: z
|
id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import type {
|
import type {
|
||||||
countryQuerySchema,
|
countryParamsSchema,
|
||||||
countryRequestSchema,
|
countryRequestSchema,
|
||||||
} from "@/modules/country/country.schemas";
|
} from "@/modules/country/country.schemas";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
export type CountryRequest = z.infer<typeof countryRequestSchema>;
|
export type CountryRequest = z.infer<typeof countryRequestSchema>;
|
||||||
|
|
||||||
export type CountryQuery = z.infer<typeof countryQuerySchema>;
|
export type CountryParams = z.infer<typeof countryParamsSchema>;
|
||||||
|
|
||||||
export type CountryResponse = {
|
export type CountryResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_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";
|
import z from "zod";
|
||||||
|
|
||||||
export const flightRequestSchema = z.object({
|
export const flightRequestSchema = z.object({
|
||||||
airline_slug: z
|
airline_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
number: z
|
number: z
|
||||||
.number("Must be number.")
|
.number("Must be number.")
|
||||||
.int("Must be integer.")
|
.int("Must be integer.")
|
||||||
.positive("Must be positive."),
|
.positive("Must be positive."),
|
||||||
departure_airport_slug: z
|
departure_airport_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
departure_terminal: z
|
departure_terminal: z
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
.max(100, "Max 100 characters.")
|
.max(100, "Max 100 characters.")
|
||||||
@@ -23,10 +23,10 @@ export const flightRequestSchema = z.object({
|
|||||||
.max(100, "Max 100 characters.")
|
.max(100, "Max 100 characters.")
|
||||||
.nullable(),
|
.nullable(),
|
||||||
departure_time: timeSchema,
|
departure_time: timeSchema,
|
||||||
arrival_airport_slug: z
|
arrival_airport_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
arrival_terminal: z
|
arrival_terminal: z
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
.max(100, "Max 100 characters.")
|
.max(100, "Max 100 characters.")
|
||||||
@@ -64,20 +64,20 @@ export const flightClassRequestSchema = z.object({
|
|||||||
.positive("Must be positive."),
|
.positive("Must be positive."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const flightQuerySchema = z.object({
|
export const flightParamsSchema = z.object({
|
||||||
slug: z
|
id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(220, "Max 220 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const flightClassQuerySchema = z.object({
|
export const flightClassParamsSchema = z.object({
|
||||||
flight_slug: z
|
flight_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(220, "Max 220 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
slug: z
|
id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.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 { AirlineResponse } from "@/modules/airline/airline.types";
|
||||||
import type { AirportResponse } from "@/modules/airport/airport.types";
|
import type { AirportResponse } from "@/modules/airport/airport.types";
|
||||||
import type {
|
import type {
|
||||||
flightClassQuerySchema,
|
flightClassParamsSchema,
|
||||||
flightClassRequestSchema,
|
flightClassRequestSchema,
|
||||||
flightQuerySchema,
|
flightParamsSchema,
|
||||||
flightRequestSchema,
|
flightRequestSchema,
|
||||||
} from "@/modules/flight/flight.schemas";
|
} from "@/modules/flight/flight.schemas";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
@@ -12,13 +12,12 @@ export type FlightRequest = z.infer<typeof flightRequestSchema>;
|
|||||||
|
|
||||||
export type FlightClassRequest = z.infer<typeof flightClassRequestSchema>;
|
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 = {
|
export type FlightResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
airline: AirlineResponse;
|
airline: AirlineResponse;
|
||||||
number: number;
|
number: number;
|
||||||
departure_airport: AirportResponse;
|
departure_airport: AirportResponse;
|
||||||
@@ -37,7 +36,6 @@ export type FlightResponse = {
|
|||||||
|
|
||||||
export type FlightClassResponse = {
|
export type FlightClassResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
flight: FlightResponse;
|
flight: FlightResponse;
|
||||||
class: string;
|
class: string;
|
||||||
seat_layout: 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";
|
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({
|
export const hotelRequestSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(100, "Max 100 characters."),
|
.max(100, "Max 100 characters."),
|
||||||
city_slug: z
|
city_id: z
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(200, "Max 200 characters."),
|
||||||
@@ -34,20 +23,20 @@ export const hotelRequestSchema = z.object({
|
|||||||
google_maps_link: z
|
google_maps_link: z
|
||||||
.url("Must be valid string URL.")
|
.url("Must be valid string URL.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(500, "Max 500 characters."),
|
.max(1000, "Max 1000 characters."),
|
||||||
google_maps_embed: z
|
google_maps_embed: z
|
||||||
.url("Must be valid string URL.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(500, "Max 500 characters."),
|
.max(1000, "Max 1000 characters."),
|
||||||
google_reviews_link: z
|
google_reviews_link: z
|
||||||
.url("Must be valid string URL.")
|
.url("Must be valid string URL.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(500, "Max 500 characters."),
|
.max(1000, "Max 1000 characters."),
|
||||||
description: z
|
description: z
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(1000, "Max 1000 characters."),
|
.max(1000, "Max 1000 characters."),
|
||||||
facility_slugs: z
|
facility_ids: z
|
||||||
.array(
|
.array(
|
||||||
z
|
z
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
@@ -81,15 +70,8 @@ export const hotelRequestSchema = z.object({
|
|||||||
.max(100, "Max 100 characters."),
|
.max(100, "Max 100 characters."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const hotelFacilityQuerySchema = z.object({
|
export const hotelParamsSchema = z.object({
|
||||||
slug: z
|
id: z
|
||||||
.string("Must be string.")
|
|
||||||
.nonempty("Must not empty.")
|
|
||||||
.max(200, "Max 200 characters."),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const hotelQuerySchema = z.object({
|
|
||||||
slug: z
|
|
||||||
.string("Must be string.")
|
.string("Must be string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(200, "Max 200 characters."),
|
||||||
|
|||||||
@@ -1,34 +1,21 @@
|
|||||||
import type { CityResponse } from "@/modules/city/city.types";
|
import type { CityResponse } from "@/modules/city/city.types";
|
||||||
|
import type { HotelFacilityResponse } from "@/modules/hotel-facility/hotel-facility.types";
|
||||||
import type {
|
import type {
|
||||||
hotelFacilityRequestSchema,
|
hotelParamsSchema,
|
||||||
hotelQuerySchema,
|
|
||||||
hotelRequestSchema,
|
hotelRequestSchema,
|
||||||
} from "@/modules/hotel/hotel.schemas";
|
} from "@/modules/hotel/hotel.schemas";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
export type HotelFacilityRequest = z.infer<typeof hotelFacilityRequestSchema>;
|
|
||||||
|
|
||||||
export type HotelRequest = z.infer<typeof hotelRequestSchema>;
|
export type HotelRequest = z.infer<typeof hotelRequestSchema>;
|
||||||
|
|
||||||
export type HotelFacilityQuery = z.infer<typeof hotelFacilityRequestSchema>;
|
export type HotelParams = z.infer<typeof hotelParamsSchema>;
|
||||||
|
|
||||||
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 HotelResponse = {
|
export type HotelResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
city: CityResponse;
|
city: CityResponse;
|
||||||
star: number;
|
star: number;
|
||||||
|
images: string[];
|
||||||
google_maps_link: string;
|
google_maps_link: string;
|
||||||
google_maps_embed: string;
|
google_maps_embed: string;
|
||||||
google_reviews_link: 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({
|
export const packageDetailRequestSchema = z.object({
|
||||||
departure_date: dateSchema,
|
departure_date: dateSchema,
|
||||||
tour_flight_slugs: z
|
tour_flight_ids: z
|
||||||
.array(
|
.array(
|
||||||
z
|
z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.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.",
|
"Must be array.",
|
||||||
)
|
)
|
||||||
.nonempty("Must not empty."),
|
.nonempty("Must not empty."),
|
||||||
outbound_flight_slugs: z
|
inbound_flight_ids: z
|
||||||
.array(
|
.array(
|
||||||
z
|
z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(420, "Max 420 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
"Must be array.",
|
"Must be array.",
|
||||||
)
|
)
|
||||||
.nonempty("Must not empty."),
|
.nonempty("Must not empty."),
|
||||||
inbound_flight_slugs: z
|
tour_hotels: z
|
||||||
.array(
|
.array(
|
||||||
z
|
z.object(
|
||||||
.string("Must be string.")
|
{
|
||||||
.nonempty("Must not empty.")
|
hotel_id: z
|
||||||
.max(420, "Max 420 characters."),
|
.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.",
|
"Must be array.",
|
||||||
)
|
)
|
||||||
.nonempty("Must not empty."),
|
.nonempty("Must not empty.")
|
||||||
tour_hotels: z.array(
|
.nullable(),
|
||||||
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.",
|
|
||||||
),
|
|
||||||
makkah_hotel: z.object(
|
makkah_hotel: z.object(
|
||||||
{
|
{
|
||||||
hotel_slug: z
|
hotel_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
check_in: timeSchema,
|
check_in: timeSchema,
|
||||||
check_out: timeSchema,
|
check_out: timeSchema,
|
||||||
},
|
},
|
||||||
@@ -71,19 +75,19 @@ export const packageDetailRequestSchema = z.object({
|
|||||||
),
|
),
|
||||||
madinah_hotel: z.object(
|
madinah_hotel: z.object(
|
||||||
{
|
{
|
||||||
hotel_slug: z
|
hotel_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
check_in: timeSchema,
|
check_in: timeSchema,
|
||||||
check_out: timeSchema,
|
check_out: timeSchema,
|
||||||
},
|
},
|
||||||
"Must be object.",
|
"Must be object.",
|
||||||
),
|
),
|
||||||
transportation_slug: z
|
transportation_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
quad_price: z
|
quad_price: z
|
||||||
.number("Must be number.")
|
.number("Must be number.")
|
||||||
.int("Must be integer.")
|
.int("Must be integer.")
|
||||||
@@ -101,22 +105,97 @@ export const packageDetailRequestSchema = z.object({
|
|||||||
.int("Must be integer.")
|
.int("Must be integer.")
|
||||||
.positive("Must be positive.")
|
.positive("Must be positive.")
|
||||||
.nullable(),
|
.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({
|
export const packageParamsSchema = z.object({
|
||||||
detail_slug: z
|
id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
slug: z
|
|
||||||
.string("Must be string.")
|
|
||||||
.nonempty("Must not empty.")
|
|
||||||
.max(200, "Max 200 characters."),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const packageDetailQuerySchema = z.object({
|
export const packageDetailParamsSchema = z.object({
|
||||||
slug: z
|
package_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.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 { FlightClassResponse } from "@/modules/flight/flight.types";
|
||||||
import type { HotelResponse } from "@/modules/hotel/hotel.types";
|
import type { HotelResponse } from "@/modules/hotel/hotel.types";
|
||||||
import type {
|
import type {
|
||||||
packageDetailQuerySchema,
|
packageDetailParamsSchema,
|
||||||
packageDetailRequestSchema,
|
packageDetailRequestSchema,
|
||||||
packageQuerySchema,
|
packageParamsSchema,
|
||||||
packageRequestSchema,
|
packageRequestSchema,
|
||||||
} from "@/modules/package/package.schemas";
|
} from "@/modules/package/package.schemas";
|
||||||
import type { TransportationClassResponse } from "@/modules/transportation/transportation.types";
|
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 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 = {
|
export type PackageResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
type: "reguler" | "plus";
|
type: "reguler" | "plus";
|
||||||
class: "silver" | "gold" | "platinum";
|
class: "silver" | "gold" | "platinum";
|
||||||
@@ -35,22 +34,51 @@ export type PackageHotelResponse = {
|
|||||||
check_out: string;
|
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 = {
|
export type PackageDetailResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
package: PackageResponse;
|
package: PackageResponse;
|
||||||
departure_date: string;
|
departure_date: string;
|
||||||
tour_flights: FlightClassResponse[];
|
tour_flights: FlightClassResponse[] | null;
|
||||||
outbound_flights: FlightClassResponse[];
|
outbound_flights: FlightClassResponse[];
|
||||||
inbound_flights: FlightClassResponse[];
|
inbound_flights: FlightClassResponse[];
|
||||||
tour_hotels: PackageHotelResponse[];
|
tour_hotels: PackageHotelResponse[] | null;
|
||||||
makkah_hotel: PackageHotelResponse;
|
makkah_hotel: PackageHotelResponse;
|
||||||
medina_hotel: PackageHotelResponse;
|
madinah_hotel: PackageHotelResponse;
|
||||||
transportation: TransportationClassResponse;
|
transportation: TransportationClassResponse;
|
||||||
quad_price: number;
|
quad_price: number;
|
||||||
triple_price: number;
|
triple_price: number;
|
||||||
double_price: number;
|
double_price: number;
|
||||||
infant_price: number | null;
|
infant_price: number | null;
|
||||||
|
itineraries: PackageItineraryResponse[];
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_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."),
|
.nonempty("Must not empty."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const transportationQuerySchema = z.object({
|
export const transportationParamsSchema = z.object({
|
||||||
slug: z
|
id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const transportationClassQuerySchema = z.object({
|
export const transportationClassParamsSchema = z.object({
|
||||||
transportation_slug: z
|
transportation_id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(200, "Max 200 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
slug: z
|
id: z
|
||||||
.string("Must be string.")
|
.ulid("Must be ulid string.")
|
||||||
.nonempty("Must not empty.")
|
.nonempty("Must not empty.")
|
||||||
.max(400, "Max 400 characters."),
|
.max(30, "Max 30 characters."),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
transportationClassQuerySchema,
|
transportationClassParamsSchema,
|
||||||
transportationClassRequestSchema,
|
transportationClassRequestSchema,
|
||||||
transportationQuerySchema,
|
transportationParamsSchema,
|
||||||
transportationRequestSchema,
|
transportationRequestSchema,
|
||||||
} from "@/modules/transportation/transportation.schemas";
|
} from "@/modules/transportation/transportation.schemas";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
@@ -12,23 +12,22 @@ export type TransportationClassRequest = z.infer<
|
|||||||
typeof transportationClassRequestSchema
|
typeof transportationClassRequestSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type TransportationQuery = z.infer<typeof transportationQuerySchema>;
|
export type TransportationParams = z.infer<typeof transportationParamsSchema>;
|
||||||
|
|
||||||
export type TransportationClassQuery = z.infer<
|
export type TransportationClassParams = z.infer<
|
||||||
typeof transportationClassQuerySchema
|
typeof transportationClassParamsSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type TransportationResponse = {
|
export type TransportationResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
type: string;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TransportationClassResponse = {
|
export type TransportationClassResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
|
||||||
transportation: TransportationResponse;
|
transportation: TransportationResponse;
|
||||||
class: string;
|
class: string;
|
||||||
total_seats: number;
|
total_seats: number;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ process.on("unhandledRejection", (reason) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const application = new Application();
|
const application = new Application();
|
||||||
|
application.initializeServices();
|
||||||
application.initializeMiddlewares();
|
application.initializeMiddlewares();
|
||||||
application.initializeRouters();
|
application.initializeRouters();
|
||||||
application.initializeErrorHandlers();
|
application.initializeErrorHandlers();
|
||||||
|
|||||||
Reference in New Issue
Block a user