From e6386648beff8bd44ef2edd75cfd2005cbe349a4 Mon Sep 17 00:00:00 2001 From: ItsMalma Date: Sat, 8 Nov 2025 13:51:18 +0700 Subject: [PATCH] add types and schemas --- bun.lock | 26 +++ package.json | 2 + src/common/schemas.ts | 34 ++++ src/database/entities/airline.entity.ts | 8 +- src/database/entities/airport.entity.ts | 8 +- src/database/entities/flight-class.entity.ts | 2 +- .../entities/hotel-schedule.entity.ts | 44 +++++ src/database/entities/hotel.entity.ts | 10 - .../entities/package-detail.entity.ts | 84 ++++----- .../entities/transportation-class.entity.ts | 2 +- .../migrations/.snapshot-goumrah.json | 174 ++++++++++++++---- .../migrations/Migration20251108053920.ts | 91 +++++++++ src/modules/airline/airline.schemas.ts | 29 +++ src/modules/airline/airline.types.ts | 21 +++ src/modules/airport/airport.schemas.ts | 23 +++ src/modules/airport/airport.types.ts | 20 ++ src/modules/city/city.schemas.ts | 19 ++ src/modules/city/city.types.ts | 19 ++ src/modules/country/country.schemas.ts | 15 ++ src/modules/country/country.types.ts | 17 ++ src/modules/flight/flight.schemas.ts | 83 +++++++++ src/modules/flight/flight.types.ts | 48 +++++ src/modules/hotel/hotel.schemas.ts | 96 ++++++++++ src/modules/hotel/hotel.types.ts | 45 +++++ src/modules/package/package.schemas.ts | 122 ++++++++++++ src/modules/package/package.types.ts | 56 ++++++ .../transportation/transportation.schemas.ts | 47 +++++ .../transportation/transportation.types.ts | 38 ++++ 28 files changed, 1082 insertions(+), 101 deletions(-) create mode 100644 src/common/schemas.ts create mode 100644 src/database/entities/hotel-schedule.entity.ts create mode 100644 src/database/migrations/Migration20251108053920.ts create mode 100644 src/modules/airline/airline.schemas.ts create mode 100644 src/modules/airline/airline.types.ts create mode 100644 src/modules/airport/airport.schemas.ts create mode 100644 src/modules/airport/airport.types.ts create mode 100644 src/modules/city/city.schemas.ts create mode 100644 src/modules/city/city.types.ts create mode 100644 src/modules/country/country.schemas.ts create mode 100644 src/modules/country/country.types.ts create mode 100644 src/modules/flight/flight.schemas.ts create mode 100644 src/modules/flight/flight.types.ts create mode 100644 src/modules/hotel/hotel.schemas.ts create mode 100644 src/modules/hotel/hotel.types.ts create mode 100644 src/modules/package/package.schemas.ts create mode 100644 src/modules/package/package.types.ts create mode 100644 src/modules/transportation/transportation.schemas.ts create mode 100644 src/modules/transportation/transportation.types.ts diff --git a/bun.lock b/bun.lock index 5067228..8654f63 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,9 @@ "@mikro-orm/postgresql": "6.5.9", "compression": "1.8.1", "cors": "2.8.5", + "date-fns": "4.1.0", "express": "5.1.0", + "file-type": "21.0.0", "helmet": "8.1.0", "reflect-metadata": "0.2.2", "zod": "4.1.12", @@ -25,6 +27,8 @@ }, }, "packages": { + "@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="], + "@jercle/yargonaut": ["@jercle/yargonaut@1.1.5", "", { "dependencies": { "chalk": "^4.1.2", "figlet": "^1.5.2", "parent-require": "^1.0.0" } }, "sha512-zBp2myVvBHp1UaJsNTyS6q4UDKT7eRiqTS4oNTS6VQMd6mpxYOdbeK4pY279cDCdakGy6hG0J3ejoXZVsPwHqw=="], "@mikro-orm/cli": ["@mikro-orm/cli@6.5.9", "", { "dependencies": { "@jercle/yargonaut": "1.1.5", "@mikro-orm/core": "6.5.9", "@mikro-orm/knex": "6.5.9", "fs-extra": "11.3.2", "tsconfig-paths": "4.2.0", "yargs": "17.7.2" }, "bin": { "mikro-orm": "cli", "mikro-orm-esm": "esm" } }, "sha512-Jihukq7STU5ZfRdjOBSR7mzOClqkhkic7t8GF/OAoMBeHgpUc93Ug2GvSMrJaNWGg4AKtgch/cP/KGv/3QtSsQ=="], @@ -49,6 +53,10 @@ "@rushstack/ts-command-line": ["@rushstack/ts-command-line@4.23.7", "", { "dependencies": { "@rushstack/terminal": "0.15.2", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "sha512-Gr9cB7DGe6uz5vq2wdr89WbVDKz0UeuFEn5H2CfWDe7JvjFFaiV15gi6mqDBTbHhHCWS7w8mF1h3BnIfUndqdA=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="], "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], @@ -139,6 +147,8 @@ "dataloader": ["dataloader@2.2.3", "", {}, "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA=="], + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -181,8 +191,12 @@ "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "figlet": ["figlet@1.9.3", "", { "dependencies": { "commander": "^14.0.0" }, "bin": { "figlet": "bin/index.js" } }, "sha512-majPgOpVtrZN1iyNGbsUP6bOtZ6eaJgg5HHh0vFvm5DJhh8dc+FJpOC4GABvMZ/A7XHAJUuJujhgUY/2jPWgMA=="], + "file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], @@ -225,6 +239,8 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-lazy": ["import-lazy@4.0.0", "", {}, "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw=="], @@ -401,6 +417,8 @@ "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -413,6 +431,8 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="], + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -421,6 +441,8 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "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=="], @@ -451,6 +473,8 @@ "@rushstack/terminal/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -481,6 +505,8 @@ "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "express/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], diff --git a/package.json b/package.json index 0e57325..eb610c9 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "@mikro-orm/postgresql": "6.5.9", "compression": "1.8.1", "cors": "2.8.5", + "date-fns": "4.1.0", "express": "5.1.0", + "file-type": "21.0.0", "helmet": "8.1.0", "reflect-metadata": "0.2.2", "zod": "4.1.12" diff --git a/src/common/schemas.ts b/src/common/schemas.ts new file mode 100644 index 0000000..cbafccd --- /dev/null +++ b/src/common/schemas.ts @@ -0,0 +1,34 @@ +import { isValid, parse } from "date-fns"; +import z from "zod"; + +export const timeSchema = z + .string("Must be string.") + .nonempty("Must not be empty.") + .transform((val, ctx) => { + const parsedDate = parse(val, "HH:mm", new Date()); + if (!isValid(parsedDate)) { + ctx.issues.push({ + code: "custom", + message: "Must be in 'HH:mm' format.", + input: val, + }); + return z.NEVER; + } + return val; + }); + +export const dateSchema = z + .string("Must be string.") + .nonempty("Must not be empty.") + .transform((val, ctx) => { + const parsedDate = parse(val, "YYYY-MM-DD", new Date()); + if (!isValid(parsedDate)) { + ctx.issues.push({ + code: "custom", + message: "Must be in 'YYYY-MM-DD' format.", + input: val, + }); + return z.NEVER; + } + return val; + }); diff --git a/src/database/entities/airline.entity.ts b/src/database/entities/airline.entity.ts index 9200ff8..c5bffc6 100644 --- a/src/database/entities/airline.entity.ts +++ b/src/database/entities/airline.entity.ts @@ -15,10 +15,6 @@ export class Airline { @PrimaryKey({ type: "varchar", length: 30 }) id!: string; - @Property({ type: "varchar", length: 10 }) - @Unique() - code!: string; - @Property({ type: "varchar", length: 200 }) @Unique() slug!: string; @@ -26,6 +22,10 @@ export class Airline { @Property({ type: "varchar", length: 100 }) name!: string; + @Property({ type: "varchar", length: 10 }) + @Unique() + code!: string; + @Property({ type: "varchar", length: 100 }) @Unique() logo!: string; diff --git a/src/database/entities/airport.entity.ts b/src/database/entities/airport.entity.ts index 24715d1..4110d07 100644 --- a/src/database/entities/airport.entity.ts +++ b/src/database/entities/airport.entity.ts @@ -16,10 +16,6 @@ export class Airport { @PrimaryKey({ type: "varchar", length: 30 }) id!: string; - @Property({ type: "varchar", length: 10 }) - @Unique() - code!: string; - @Property({ type: "varchar", length: 200 }) @Unique() slug!: string; @@ -27,6 +23,10 @@ export class Airport { @Property({ type: "varchar", length: 100 }) name!: string; + @Property({ type: "varchar", length: 10 }) + @Unique() + code!: string; + @ManyToOne(() => City) city!: Rel; diff --git a/src/database/entities/flight-class.entity.ts b/src/database/entities/flight-class.entity.ts index 298351f..9e30cd2 100644 --- a/src/database/entities/flight-class.entity.ts +++ b/src/database/entities/flight-class.entity.ts @@ -17,7 +17,7 @@ export class FlightClass { @PrimaryKey({ type: "varchar", length: 30 }) id!: string; - @Property({ type: "varchar", length: 420 }) + @Property({ type: "varchar", length: 200 }) @Unique() slug!: string; diff --git a/src/database/entities/hotel-schedule.entity.ts b/src/database/entities/hotel-schedule.entity.ts new file mode 100644 index 0000000..c9b41c6 --- /dev/null +++ b/src/database/entities/hotel-schedule.entity.ts @@ -0,0 +1,44 @@ +import { Hotel } from "@/database/entities/hotel.entity"; +import { PackageDetail } from "@/database/entities/package-detail.entity"; +import { + Collection, + Entity, + ManyToMany, + ManyToOne, + PrimaryKey, + Property, + type Rel, +} from "@mikro-orm/core"; + +@Entity() +export class HotelSchedule { + @PrimaryKey({ type: "varchar", length: 30 }) + id!: string; + + @ManyToOne(() => Hotel) + hotel!: Rel; + + @Property({ type: "time" }) + checkIn!: string; + + @Property({ type: "time" }) + checkOut!: string; + + @Property({ + type: "timestamp", + onCreate: () => new Date(), + }) + createdAt!: Date; + + @Property({ + type: "timestamp", + onCreate: () => new Date(), + onUpdate: () => new Date(), + }) + updatedAt!: Date; + + // Inverse side + + @ManyToMany(() => PackageDetail, (packageDetail) => packageDetail.tourHotels) + tourPackageDetails = new Collection(this); +} diff --git a/src/database/entities/hotel.entity.ts b/src/database/entities/hotel.entity.ts index adbb049..ef9257d 100644 --- a/src/database/entities/hotel.entity.ts +++ b/src/database/entities/hotel.entity.ts @@ -1,7 +1,6 @@ import { City } from "@/database/entities/city.entity"; import { HotelFacility } from "@/database/entities/hotel-facility.entity"; import { HotelImage } from "@/database/entities/hotel-image.entity"; -import { PackageDetail } from "@/database/entities/package-detail.entity"; import { Collection, Entity, @@ -84,13 +83,4 @@ export class Hotel { owner: true, }) facilities = new Collection(this); - - @ManyToMany(() => PackageDetail, (packageDetail) => packageDetail.tourHotels) - tourPackageDetails = new Collection(this); - - @OneToMany(() => PackageDetail, (packageDetail) => packageDetail.makkahHotel) - makkahPackageDetails = new Collection(this); - - @OneToMany(() => PackageDetail, (packageDetail) => packageDetail.madinahHotel) - madinahPackageDetails = new Collection(this); } diff --git a/src/database/entities/package-detail.entity.ts b/src/database/entities/package-detail.entity.ts index 8b95016..f41afad 100644 --- a/src/database/entities/package-detail.entity.ts +++ b/src/database/entities/package-detail.entity.ts @@ -1,5 +1,5 @@ import { FlightClass } from "@/database/entities/flight-class.entity"; -import { Hotel } from "@/database/entities/hotel.entity"; +import { HotelSchedule } from "@/database/entities/hotel-schedule.entity"; import { Package } from "@/database/entities/package.entity"; import { TransportationClass } from "@/database/entities/transportation-class.entity"; import { @@ -29,42 +29,6 @@ export class PackageDetail { @Property({ type: "date" }) departureDate!: Date; - @ManyToOne(() => Hotel) - makkahHotel!: Rel; - - @ManyToOne(() => Hotel) - madinahHotel!: Rel; - - @ManyToOne(() => TransportationClass) - transportation!: Rel; - - @Property({ type: "decimal", unsigned: true }) - quadPrice!: number; - - @Property({ type: "decimal", unsigned: true }) - triplePrice!: number; - - @Property({ type: "decimal", unsigned: true }) - doublePrice!: number; - - @Property({ type: "decimal", nullable: true, unsigned: true }) - infantPrice?: number; - - @Property({ - type: "timestamp", - onCreate: () => new Date(), - }) - createdAt!: Date; - - @Property({ - type: "timestamp", - onCreate: () => new Date(), - onUpdate: () => new Date(), - }) - updatedAt!: Date; - - // Collections - @ManyToMany( () => FlightClass, (flightClass) => flightClass.tourPackageDetails, @@ -95,9 +59,47 @@ export class PackageDetail { ) inboundFlightClasses = new Collection(this); - @ManyToMany(() => Hotel, (hotel) => hotel.tourPackageDetails, { - owner: true, - fixedOrder: true, + @ManyToMany( + () => HotelSchedule, + (hotelSchedule) => hotelSchedule.tourPackageDetails, + { + owner: true, + fixedOrder: true, + }, + ) + tourHotels = new Collection(this); + + @ManyToOne(() => HotelSchedule) + makkahHotel!: Rel; + + @ManyToOne(() => HotelSchedule) + madinahHotel!: Rel; + + @ManyToOne(() => TransportationClass) + transportation!: Rel; + + @Property({ type: "decimal", unsigned: true }) + quadPrice!: number; + + @Property({ type: "decimal", unsigned: true }) + triplePrice!: number; + + @Property({ type: "decimal", unsigned: true }) + doublePrice!: number; + + @Property({ type: "decimal", nullable: true, unsigned: true }) + infantPrice?: number; + + @Property({ + type: "timestamp", + onCreate: () => new Date(), }) - tourHotels = new Collection(this); + createdAt!: Date; + + @Property({ + type: "timestamp", + onCreate: () => new Date(), + onUpdate: () => new Date(), + }) + updatedAt!: Date; } diff --git a/src/database/entities/transportation-class.entity.ts b/src/database/entities/transportation-class.entity.ts index 7fd6498..dc62755 100644 --- a/src/database/entities/transportation-class.entity.ts +++ b/src/database/entities/transportation-class.entity.ts @@ -18,7 +18,7 @@ export class TransportationClass { @PrimaryKey({ type: "varchar", length: 30 }) id!: string; - @Property({ type: "varchar", length: 400 }) + @Property({ type: "varchar", length: 200 }) @Unique() slug!: string; diff --git a/src/database/migrations/.snapshot-goumrah.json b/src/database/migrations/.snapshot-goumrah.json index bc2b803..7cbc149 100644 --- a/src/database/migrations/.snapshot-goumrah.json +++ b/src/database/migrations/.snapshot-goumrah.json @@ -16,16 +16,6 @@ "length": 30, "mappedType": "string" }, - "code": { - "name": "code", - "type": "varchar(10)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 10, - "mappedType": "string" - }, "slug": { "name": "slug", "type": "varchar(200)", @@ -46,6 +36,16 @@ "length": 100, "mappedType": "string" }, + "code": { + "name": "code", + "type": "varchar(10)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 10, + "mappedType": "string" + }, "logo": { "name": "logo", "type": "varchar(100)", @@ -104,20 +104,20 @@ "indexes": [ { "columnNames": [ - "code" + "slug" ], "composite": false, - "keyName": "airline_code_unique", + "keyName": "airline_slug_unique", "constraint": true, "primary": false, "unique": true }, { "columnNames": [ - "slug" + "code" ], "composite": false, - "keyName": "airline_slug_unique", + "keyName": "airline_code_unique", "constraint": true, "primary": false, "unique": true @@ -344,16 +344,6 @@ "length": 30, "mappedType": "string" }, - "code": { - "name": "code", - "type": "varchar(10)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 10, - "mappedType": "string" - }, "slug": { "name": "slug", "type": "varchar(200)", @@ -374,6 +364,16 @@ "length": 100, "mappedType": "string" }, + "code": { + "name": "code", + "type": "varchar(10)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 10, + "mappedType": "string" + }, "city_id": { "name": "city_id", "type": "varchar(30)", @@ -410,20 +410,20 @@ "indexes": [ { "columnNames": [ - "code" + "slug" ], "composite": false, - "keyName": "airport_code_unique", + "keyName": "airport_slug_unique", "constraint": true, "primary": false, "unique": true }, { "columnNames": [ - "slug" + "code" ], "composite": false, - "keyName": "airport_slug_unique", + "keyName": "airport_code_unique", "constraint": true, "primary": false, "unique": true @@ -697,12 +697,12 @@ }, "slug": { "name": "slug", - "type": "varchar(420)", + "type": "varchar(200)", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "length": 420, + "length": 200, "mappedType": "string" }, "flight_id": { @@ -1283,6 +1283,100 @@ }, "nativeEnums": {} }, + { + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 30, + "mappedType": "string" + }, + "hotel_id": { + "name": "hotel_id", + "type": "varchar(30)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 30, + "mappedType": "string" + }, + "check_in": { + "name": "check_in", + "type": "time(0)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 0, + "mappedType": "time" + }, + "check_out": { + "name": "check_out", + "type": "time(0)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 0, + "mappedType": "time" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "hotel_schedule", + "schema": "public", + "indexes": [ + { + "keyName": "hotel_schedule_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "hotel_schedule_hotel_id_foreign": { + "constraintName": "hotel_schedule_hotel_id_foreign", + "columnNames": [ + "hotel_id" + ], + "localTableName": "public.hotel_schedule", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.hotel", + "updateRule": "cascade" + } + }, + "nativeEnums": {} + }, { "columns": { "id": { @@ -1525,12 +1619,12 @@ }, "slug": { "name": "slug", - "type": "varchar(400)", + "type": "varchar(200)", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "length": 400, + "length": 200, "mappedType": "string" }, "transportation_id": { @@ -1830,7 +1924,7 @@ "referencedColumnNames": [ "id" ], - "referencedTableName": "public.hotel", + "referencedTableName": "public.hotel_schedule", "updateRule": "cascade" }, "package_detail_madinah_hotel_id_foreign": { @@ -1842,7 +1936,7 @@ "referencedColumnNames": [ "id" ], - "referencedTableName": "public.hotel", + "referencedTableName": "public.hotel_schedule", "updateRule": "cascade" }, "package_detail_transportation_id_foreign": { @@ -1881,8 +1975,8 @@ "length": 30, "mappedType": "string" }, - "hotel_id": { - "name": "hotel_id", + "hotel_schedule_id": { + "name": "hotel_schedule_id", "type": "varchar(30)", "unsigned": false, "autoincrement": false, @@ -1921,16 +2015,16 @@ "deleteRule": "cascade", "updateRule": "cascade" }, - "package_detail_tour_hotels_hotel_id_foreign": { - "constraintName": "package_detail_tour_hotels_hotel_id_foreign", + "package_detail_tour_hotels_hotel_schedule_id_foreign": { + "constraintName": "package_detail_tour_hotels_hotel_schedule_id_foreign", "columnNames": [ - "hotel_id" + "hotel_schedule_id" ], "localTableName": "public.package_detail_tour_hotels", "referencedColumnNames": [ "id" ], - "referencedTableName": "public.hotel", + "referencedTableName": "public.hotel_schedule", "deleteRule": "cascade", "updateRule": "cascade" } diff --git a/src/database/migrations/Migration20251108053920.ts b/src/database/migrations/Migration20251108053920.ts new file mode 100644 index 0000000..1bf17a7 --- /dev/null +++ b/src/database/migrations/Migration20251108053920.ts @@ -0,0 +1,91 @@ +import { Migration } from "@mikro-orm/migrations"; + +export class Migration20251108053920 extends Migration { + override async up(): Promise { + this.addSql( + `create table "hotel_schedule" ("id" varchar(30) not null, "hotel_id" varchar(30) not null, "check_in" time(0) not null, "check_out" time(0) not null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "hotel_schedule_pkey" primary key ("id"));`, + ); + + this.addSql( + `alter table "hotel_schedule" add constraint "hotel_schedule_hotel_id_foreign" foreign key ("hotel_id") references "hotel" ("id") on update cascade;`, + ); + + 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_tour_hotels" drop constraint "package_detail_tour_hotels_hotel_id_foreign";`, + ); + + this.addSql( + `alter table "flight_class" alter column "slug" type varchar(200) using ("slug"::varchar(200));`, + ); + + this.addSql( + `alter table "transportation_class" alter column "slug" type varchar(200) using ("slug"::varchar(200));`, + ); + + 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_tour_hotels" rename column "hotel_id" to "hotel_schedule_id";`, + ); + this.addSql( + `alter table "package_detail_tour_hotels" add constraint "package_detail_tour_hotels_hotel_schedule_id_foreign" foreign key ("hotel_schedule_id") references "hotel_schedule" ("id") on update cascade on delete cascade;`, + ); + } + + override async down(): Promise { + 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_tour_hotels" drop constraint "package_detail_tour_hotels_hotel_schedule_id_foreign";`, + ); + + this.addSql(`drop table if exists "hotel_schedule" cascade;`); + + 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 "flight_class" alter column "slug" type varchar(420) using ("slug"::varchar(420));`, + ); + + this.addSql( + `alter table "transportation_class" alter column "slug" type varchar(400) using ("slug"::varchar(400));`, + ); + + this.addSql( + `alter table "package_detail" add constraint "package_detail_makkah_hotel_id_foreign" foreign key ("makkah_hotel_id") references "hotel" ("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" ("id") on update cascade;`, + ); + + this.addSql( + `alter table "package_detail_tour_hotels" rename column "hotel_schedule_id" to "hotel_id";`, + ); + this.addSql( + `alter table "package_detail_tour_hotels" add constraint "package_detail_tour_hotels_hotel_id_foreign" foreign key ("hotel_id") references "hotel" ("id") on update cascade on delete cascade;`, + ); + } +} diff --git a/src/modules/airline/airline.schemas.ts b/src/modules/airline/airline.schemas.ts new file mode 100644 index 0000000..013f9eb --- /dev/null +++ b/src/modules/airline/airline.schemas.ts @@ -0,0 +1,29 @@ +import z from "zod"; + +export const airlineRequestSchema = z.object({ + name: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + code: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(10, "Max 10 characters."), + logo: z.base64("Must be base64 string.").nonempty("Must not empty."), + skytrax_rating: z + .number("Must be number.") + .int("Must be integer.") + .min(1, "Minimum 1.") + .max(5, "Maximum 5."), + skytrax_type: z.enum( + ["full_service", "low_cost"], + "Must be either 'full_service' or 'low_cost'.", + ), +}); + +export const airlineQuerySchema = z.object({ + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); diff --git a/src/modules/airline/airline.types.ts b/src/modules/airline/airline.types.ts new file mode 100644 index 0000000..7c5ec1b --- /dev/null +++ b/src/modules/airline/airline.types.ts @@ -0,0 +1,21 @@ +import type { + airlineQuerySchema, + airlineRequestSchema, +} from "@/modules/airline/airline.schemas"; +import z from "zod"; + +export type AirlineRequest = z.infer; + +export type AirlineQuery = z.infer; + +export type AirlineResponse = { + id: string; + slug: string; + name: string; + code: string; + logo: string; + skytrax_rating: number; + skytrax_type: "full_service" | "low_cost"; + created_at: Date; + updated_at: Date; +}; diff --git a/src/modules/airport/airport.schemas.ts b/src/modules/airport/airport.schemas.ts new file mode 100644 index 0000000..c6ceb57 --- /dev/null +++ b/src/modules/airport/airport.schemas.ts @@ -0,0 +1,23 @@ +import z from "zod"; + +export const airportRequestSchema = z.object({ + name: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + code: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(10, "Max 10 characters."), + city_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); + +export const airportQuerySchema = z.object({ + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); diff --git a/src/modules/airport/airport.types.ts b/src/modules/airport/airport.types.ts new file mode 100644 index 0000000..900a8d1 --- /dev/null +++ b/src/modules/airport/airport.types.ts @@ -0,0 +1,20 @@ +import type { + airportQuerySchema, + airportRequestSchema, +} from "@/modules/airport/airport.schemas"; +import type { CityResponse } from "@/modules/city/city.types"; +import z from "zod"; + +export type AirportRequest = z.infer; + +export type AirportQuery = z.infer; + +export type AirportResponse = { + id: string; + slug: string; + name: string; + code: string; + city: CityResponse; + created_at: Date; + updated_at: Date; +}; diff --git a/src/modules/city/city.schemas.ts b/src/modules/city/city.schemas.ts new file mode 100644 index 0000000..9e40c39 --- /dev/null +++ b/src/modules/city/city.schemas.ts @@ -0,0 +1,19 @@ +import z from "zod"; + +export const cityRequestSchema = z.object({ + name: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + country_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); + +export const cityQuerySchema = z.object({ + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); diff --git a/src/modules/city/city.types.ts b/src/modules/city/city.types.ts new file mode 100644 index 0000000..9da96f5 --- /dev/null +++ b/src/modules/city/city.types.ts @@ -0,0 +1,19 @@ +import type { + cityQuerySchema, + cityRequestSchema, +} from "@/modules/city/city.schemas"; +import type { CountryResponse } from "@/modules/country/country.types"; +import z from "zod"; + +export type CityRequest = z.infer; + +export type CityQuery = z.infer; + +export type CityResponse = { + id: string; + slug: string; + name: string; + country: CountryResponse; + created_at: Date; + updated_at: Date; +}; diff --git a/src/modules/country/country.schemas.ts b/src/modules/country/country.schemas.ts new file mode 100644 index 0000000..fef8d3f --- /dev/null +++ b/src/modules/country/country.schemas.ts @@ -0,0 +1,15 @@ +import z from "zod"; + +export const countryRequestSchema = z.object({ + name: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), +}); + +export const countryQuerySchema = z.object({ + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); diff --git a/src/modules/country/country.types.ts b/src/modules/country/country.types.ts new file mode 100644 index 0000000..3a68261 --- /dev/null +++ b/src/modules/country/country.types.ts @@ -0,0 +1,17 @@ +import type { + countryQuerySchema, + countryRequestSchema, +} from "@/modules/country/country.schemas"; +import z from "zod"; + +export type CountryRequest = z.infer; + +export type CountryQuery = z.infer; + +export type CountryResponse = { + id: string; + slug: string; + name: string; + created_at: Date; + updated_at: Date; +}; diff --git a/src/modules/flight/flight.schemas.ts b/src/modules/flight/flight.schemas.ts new file mode 100644 index 0000000..330ba38 --- /dev/null +++ b/src/modules/flight/flight.schemas.ts @@ -0,0 +1,83 @@ +import { timeSchema } from "@/common/schemas"; +import z from "zod"; + +export const flightRequestSchema = z.object({ + airline_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), + number: z + .number("Must be number.") + .int("Must be integer.") + .positive("Must be positive."), + departure_airport_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), + departure_terminal: z + .string("Must be string.") + .max(100, "Max 100 characters.") + .nullable(), + departure_gate: z + .string("Must be string.") + .max(100, "Max 100 characters.") + .nullable(), + departure_time: timeSchema, + arrival_airport_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), + arrival_terminal: z + .string("Must be string.") + .max(100, "Max 100 characters.") + .nullable(), + arrival_gate: z + .string("Must be string.") + .max(100, "Max 100 characters.") + .nullable(), + duration: z + .number("Must be number.") + .int("Must be integer.") + .positive("Must be positive."), + aircraft: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), +}); + +export const flightClassRequestSchema = z.object({ + class: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + seat_layout: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(10, "Max 10 characters."), + baggage: z + .number("Must be number.") + .int("Must be integer.") + .positive("Must be positive."), + cabin_baggage: z + .number("Must be number.") + .int("Must be integer.") + .positive("Must be positive."), +}); + +export const flightQuerySchema = z.object({ + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(220, "Max 220 characters."), +}); + +export const flightClassQuerySchema = z.object({ + flight_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(220, "Max 220 characters."), + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(420, "Max 420 characters."), +}); diff --git a/src/modules/flight/flight.types.ts b/src/modules/flight/flight.types.ts new file mode 100644 index 0000000..b183e99 --- /dev/null +++ b/src/modules/flight/flight.types.ts @@ -0,0 +1,48 @@ +import type { AirlineResponse } from "@/modules/airline/airline.types"; +import type { AirportResponse } from "@/modules/airport/airport.types"; +import type { + flightClassQuerySchema, + flightClassRequestSchema, + flightQuerySchema, + flightRequestSchema, +} from "@/modules/flight/flight.schemas"; +import z from "zod"; + +export type FlightRequest = z.infer; + +export type FlightClassRequest = z.infer; + +export type FlightQuery = z.infer; + +export type FlightClassQuery = z.infer; + +export type FlightResponse = { + id: string; + slug: string; + airline: AirlineResponse; + number: number; + departure_airport: AirportResponse; + departure_terminal: string | null; + departure_gate: string | null; + departure_time: string; + arrival_airport: AirportResponse; + arrival_terminal: string | null; + arrival_gate: string | null; + arrival_time: string; + duration: number; + aircraft: string; + created_at: Date; + updated_at: Date; +}; + +export type FlightClassResponse = { + id: string; + slug: string; + flight: FlightResponse; + class: string; + seat_layout: string; + baggage: number; + cabin_baggage: number; + created_at: Date; + updated_at: Date; +}; diff --git a/src/modules/hotel/hotel.schemas.ts b/src/modules/hotel/hotel.schemas.ts new file mode 100644 index 0000000..44fc23d --- /dev/null +++ b/src/modules/hotel/hotel.schemas.ts @@ -0,0 +1,96 @@ +import z from "zod"; + +export const hotelFacilityRequestSchema = z.object({ + name: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + icon: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), +}); + +export const hotelRequestSchema = z.object({ + name: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + city_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), + star: z + .number("Must be number.") + .int("Must be integer.") + .min(1, "Minimum 1.") + .max(7, "Maximum 7."), + images: z + .array( + z.base64("Must be base64 string.").nonempty("Must not empty."), + "Must be array.", + ) + .nonempty("Must not empty."), + google_maps_link: z + .url("Must be valid string URL.") + .nonempty("Must not empty.") + .max(500, "Max 500 characters."), + google_maps_embed: z + .url("Must be valid string URL.") + .nonempty("Must not empty.") + .max(500, "Max 500 characters."), + google_reviews_link: z + .url("Must be valid string URL.") + .nonempty("Must not empty.") + .max(500, "Max 500 characters."), + description: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(1000, "Max 1000 characters."), + facility_slugs: z + .array( + z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), + "Must be array.", + ) + .nonempty("Must not empty."), + address: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + landmark: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + distance_to_landmark: z + .number("Must be number.") + .positive("Must be positive."), + food_type: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + food_amount: z + .number("Must be number.") + .int("Must be integer.") + .positive("Must be positive."), + food_menu: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), +}); + +export const hotelFacilityQuerySchema = z.object({ + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); + +export const hotelQuerySchema = z.object({ + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); diff --git a/src/modules/hotel/hotel.types.ts b/src/modules/hotel/hotel.types.ts new file mode 100644 index 0000000..cbd5e68 --- /dev/null +++ b/src/modules/hotel/hotel.types.ts @@ -0,0 +1,45 @@ +import type { CityResponse } from "@/modules/city/city.types"; +import type { + hotelFacilityRequestSchema, + hotelQuerySchema, + hotelRequestSchema, +} from "@/modules/hotel/hotel.schemas"; +import z from "zod"; + +export type HotelFacilityRequest = z.infer; + +export type HotelRequest = z.infer; + +export type HotelFacilityQuery = z.infer; + +export type HotelQuery = z.infer; + +export type HotelFacilityResponse = { + id: string; + slug: string; + name: string; + icon: string; + created_at: Date; + updated_at: Date; +}; + +export type HotelResponse = { + id: string; + slug: string; + name: string; + city: CityResponse; + star: number; + google_maps_link: string; + google_maps_embed: string; + google_reviews_link: string; + description: string; + facilities: HotelFacilityResponse[]; + address: string; + landmark: string; + distance_to_landmark: number; + food_type: string; + food_amount: number; + food_menu: string; + created_at: Date; + updated_at: Date; +}; diff --git a/src/modules/package/package.schemas.ts b/src/modules/package/package.schemas.ts new file mode 100644 index 0000000..e166e8a --- /dev/null +++ b/src/modules/package/package.schemas.ts @@ -0,0 +1,122 @@ +import { dateSchema, timeSchema } from "@/common/schemas"; +import z from "zod"; + +export const packageRequestSchema = z.object({ + name: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + type: z.enum(["reguler", "plus"], "Must be either 'reguler' or 'plus'."), + class: z.enum( + ["silver", "gold", "platinum"], + "Must be either 'silver', 'gold', or 'platinum'.", + ), + thumbnail: z.base64("Must be base64 string.").nonempty("Must not empty."), + use_fast_train: z.boolean("Must be boolean."), +}); + +export const packageDetailRequestSchema = z.object({ + departure_date: dateSchema, + tour_flight_slugs: z + .array( + z + .string("Must be string.") + .nonempty("Must not empty.") + .max(420, "Max 420 characters."), + "Must be array.", + ) + .nonempty("Must not empty."), + outbound_flight_slugs: z + .array( + z + .string("Must be string.") + .nonempty("Must not empty.") + .max(420, "Max 420 characters."), + "Must be array.", + ) + .nonempty("Must not empty."), + inbound_flight_slugs: z + .array( + z + .string("Must be string.") + .nonempty("Must not empty.") + .max(420, "Max 420 characters."), + "Must be array.", + ) + .nonempty("Must not empty."), + tour_hotels: z.array( + z.object( + { + hotel_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), + check_in: timeSchema, + check_out: timeSchema, + }, + "Must be object.", + ), + "Must be array.", + ), + makkah_hotel: 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.", + ), + madinah_hotel: 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.", + ), + transportation_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), + quad_price: z + .number("Must be number.") + .int("Must be integer.") + .positive("Must be positive."), + triple_price: z + .number("Must be number.") + .int("Must be integer.") + .positive("Must be positive."), + double_price: z + .number("Must be number.") + .int("Must be integer.") + .positive("Must be positive."), + infant_price: z + .number("Must be number.") + .int("Must be integer.") + .positive("Must be positive.") + .nullable(), +}); + +export const packageQuerySchema = z.object({ + detail_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); + +export const packageDetailQuerySchema = z.object({ + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); diff --git a/src/modules/package/package.types.ts b/src/modules/package/package.types.ts new file mode 100644 index 0000000..e395362 --- /dev/null +++ b/src/modules/package/package.types.ts @@ -0,0 +1,56 @@ +import type { FlightClassResponse } from "@/modules/flight/flight.types"; +import type { HotelResponse } from "@/modules/hotel/hotel.types"; +import type { + packageDetailQuerySchema, + packageDetailRequestSchema, + packageQuerySchema, + packageRequestSchema, +} from "@/modules/package/package.schemas"; +import type { TransportationClassResponse } from "@/modules/transportation/transportation.types"; +import z from "zod"; + +export type PackageRequest = z.infer; + +export type PackageDetailRequest = z.infer; + +export type PackageQuery = z.infer; + +export type PackageDetailQuery = z.infer; + +export type PackageResponse = { + id: string; + slug: string; + name: string; + type: "reguler" | "plus"; + class: "silver" | "gold" | "platinum"; + thumbnail: string; + use_fast_train: boolean; + created_at: Date; + updated_at: Date; +}; + +export type PackageHotelResponse = { + hotel: HotelResponse; + check_in: string; + check_out: string; +}; + +export type PackageDetailResponse = { + id: string; + slug: string; + package: PackageResponse; + departure_date: string; + tour_flights: FlightClassResponse[]; + outbound_flights: FlightClassResponse[]; + inbound_flights: FlightClassResponse[]; + tour_hotels: PackageHotelResponse[]; + makkah_hotel: PackageHotelResponse; + medina_hotel: PackageHotelResponse; + transportation: TransportationClassResponse; + quad_price: number; + triple_price: number; + double_price: number; + infant_price: number | null; + created_at: Date; + updated_at: Date; +}; diff --git a/src/modules/transportation/transportation.schemas.ts b/src/modules/transportation/transportation.schemas.ts new file mode 100644 index 0000000..33ffb56 --- /dev/null +++ b/src/modules/transportation/transportation.schemas.ts @@ -0,0 +1,47 @@ +import z from "zod"; + +export const transportationRequestSchema = z.object({ + name: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + type: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), +}); + +export const transportationClassRequestSchema = z.object({ + class: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(100, "Max 100 characters."), + total_seats: z + .number("Must be number.") + .int("Must be integer.") + .positive("Must be positive."), + images: z + .array( + z.base64("Must be base64 string.").nonempty("Must not empty."), + "Must be array.", + ) + .nonempty("Must not empty."), +}); + +export const transportationQuerySchema = z.object({ + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), +}); + +export const transportationClassQuerySchema = z.object({ + transportation_slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(200, "Max 200 characters."), + slug: z + .string("Must be string.") + .nonempty("Must not empty.") + .max(400, "Max 400 characters."), +}); diff --git a/src/modules/transportation/transportation.types.ts b/src/modules/transportation/transportation.types.ts new file mode 100644 index 0000000..a805f43 --- /dev/null +++ b/src/modules/transportation/transportation.types.ts @@ -0,0 +1,38 @@ +import type { + transportationClassQuerySchema, + transportationClassRequestSchema, + transportationQuerySchema, + transportationRequestSchema, +} from "@/modules/transportation/transportation.schemas"; +import z from "zod"; + +export type TransportationRequest = z.infer; + +export type TransportationClassRequest = z.infer< + typeof transportationClassRequestSchema +>; + +export type TransportationQuery = z.infer; + +export type TransportationClassQuery = z.infer< + typeof transportationClassQuerySchema +>; + +export type TransportationResponse = { + id: string; + slug: string; + name: string; + created_at: Date; + updated_at: Date; +}; + +export type TransportationClassResponse = { + id: string; + slug: string; + transportation: TransportationResponse; + class: string; + total_seats: number; + images: string[]; + created_at: Date; + updated_at: Date; +};