diff --git a/replicate.ts b/replicate.ts index c8568d4..682e660 100644 --- a/replicate.ts +++ b/replicate.ts @@ -1,43 +1,65 @@ -// import { db } from "./src/db/index"; -// import { IUnit } from "./src/types/IUnit"; -// import { unitsTable } from "./src/db/schema/units"; -// import got from "got"; +import { db } from "./src/db/index"; +import { IUnit } from "./src/types/IUnit"; +import { unitsTable } from "./src/db/schema/units"; +import got from "got"; +// import { and, eq, exists } from "drizzle-orm"; -// const { units } = await got -// .get( -// "https://irth-test-ii-16348121.dev.odoo.com/3d-fields/1?token=f82a4708-9a4d-491a-9424-1580b7173ba6" -// ) -// .json<{ units: IUnit[] }>(); +const { units: unitsOfMarasiDrive } = await got + .get( + "https://irth-test-ii-16348121.dev.odoo.com/3d-fields/1?token=f82a4708-9a4d-491a-9424-1580b7173ba6" + ) + .json<{ units: IUnit[] }>(); -// for (const { -// no_of_bathrooms, -// floor, -// no_of_parking_space, -// project, -// sales_price, -// square_ft, -// state, -// suits_area, -// unit_no, -// unit_type, -// unit_view, -// } of units) { -// type Unit = typeof unitsTable.$inferInsert; +const { units: unitsOfDubaiMarina } = await got + .get( + "https://irth-test-ii-16348121.dev.odoo.com/3d-fields/7?token=f82a4708-9a4d-491a-9424-1580b7173ba6" + ) + .json<{ units: IUnit[] }>(); -// const unit: Unit = { -// unitNo: unit_no, -// number: +unit_no.slice(2), -// project, -// floor: +floor, -// noOfBathrooms: no_of_bathrooms, -// noOfParkingSpace: no_of_parking_space, -// salesPrice: sales_price, -// state: state as Unit["state"], -// unitType: unit_type as Unit["unitType"], -// unitView: unit_view as Unit["unitView"], -// suitsArea: suits_area.toString(), -// squareFt: square_ft.toString(), -// }; +for (const { + no_of_bathrooms, + floor, + no_of_parking_space, + project, + sales_price, + square_ft, + state, + suits_area, + unit_no, + unit_type, + unit_view, + balcony_area, +} of unitsOfMarasiDrive.concat(unitsOfDubaiMarina)) { + type Unit = typeof unitsTable.$inferInsert; -// await db.insert(unitsTable).values(unit); -// } + const parts = unit_no.split("-"); + + const unit: Unit = { + unitNo: unit_no, + number: + parts.length === 1 + ? +parts[0] + : !Number.isNaN(+parts[0]) + ? +parts[0] + : +parts[1], + project, + floor: +floor, + noOfBathrooms: no_of_bathrooms, + noOfParkingSpace: no_of_parking_space, + salesPrice: sales_price, + state: state as Unit["state"], + unitType: unit_type ? (unit_type as Unit["unitType"]) : null, + unitView: unit_view ? (unit_view as Unit["unitView"]) : null, + suitsArea: suits_area, + squareFt: square_ft, + balconyArea: balcony_area, + }; + + await db + .insert(unitsTable) + .values(unit) + .onConflictDoUpdate({ + target: [unitsTable.project, unitsTable.unitNo], + set: unit, + }); +} diff --git a/src/controllers/units.ts b/src/controllers/units.ts index 2f9500e..bb5aaa3 100644 --- a/src/controllers/units.ts +++ b/src/controllers/units.ts @@ -2,23 +2,326 @@ import Elysia, { error, t } from "elysia"; import { unitsTable } from "../db/schema"; import { createSelectSchema } from "drizzle-typebox"; import { db } from "../db"; -import { eq, and } from "drizzle-orm"; +import { + and, + asc, + between, + count, + desc, + eq, + inArray, + max, + min, +} from "drizzle-orm"; export const getUnitSchema = createSelectSchema(unitsTable); -export const unitsController = new Elysia({ prefix: "/units" }).get( - "/", - async ({ query: { project } }) => { - try { - return await db.query.unitsTable.findMany({ - where: and(project ? eq(unitsTable.project, project) : undefined), - }); - } catch (err) { - console.log((err as Error).message); - return error(500, "Internal server error"); +export const unitsController = new Elysia({ prefix: "/units" }) + .get( + "/", + async ({ + query: { + project, + unitTypes, + cost, + floor, + area, + view, + order, + offset, + limit, + }, + }) => { + try { + return await db.query.unitsTable.findMany({ + where: and( + project ? eq(unitsTable.project, project) : undefined, + unitTypes ? inArray(unitsTable.unitType, unitTypes) : undefined, + cost ? between(unitsTable.salesPrice, cost[0], cost[1]) : undefined, + floor ? between(unitsTable.floor, floor[0], floor[1]) : undefined, + area ? between(unitsTable.squareFt, area[0], area[1]) : undefined, + view ? eq(unitsTable.unitView, view) : undefined + ), + orderBy: order + ? [ + order[1] === "asc" + ? asc( + unitsTable[ + order[0] === "cost" ? "salesPrice" : "squareFt" + ] + ) + : desc( + unitsTable[ + order[0] === "cost" ? "salesPrice" : "squareFt" + ] + ), + ] + : undefined, + offset, + limit, + }); + } catch (err) { + console.log((err as Error).message); + return error(500, "Internal server error"); + } + }, + { + query: t.Partial( + t.Object({ + project: t.String(), + unitTypes: t.Array( + t.Union([ + t.Literal("Studio Squared"), + t.Literal("1 BR Squared"), + t.Literal("Studio Flex"), + t.Literal("2 BR Squared"), + t.Literal("Studio2"), + t.Literal("One Bedroom2"), + t.Literal("One Bedroom Loft"), + t.Literal("Two Bedroom Loft"), + ]) + ), + cost: t.Tuple([t.Number(), t.Number()]), + floor: t.Tuple([t.Number(), t.Number()]), + area: t.Tuple([t.Number(), t.Number()]), + view: t.Union([ + t.Literal("Canal / Amenities"), + t.Literal("Corner-Canal / Amenities"), + t.Literal("Corner-Canal View"), + t.Literal("Business Bay"), + t.Literal("Park Facing"), + t.Literal("Corner-Park Facing"), + t.Literal("Partial Park"), + t.Literal("BK/DT / Amenities"), + t.Literal("Corner-BK/DT / Amenities"), + t.Literal("Corner-Canal / BK/DT View"), + t.Literal("Marina View"), + t.Literal("Partial Marina View"), + t.Literal("Partial Marina, Partial City View"), + t.Literal("City view"), + t.Literal("Marina View, Sea View"), + t.Literal("Partial Marina View, Partial Sea View"), + ]), + order: t.Tuple([ + t.Union([t.Literal("cost"), t.Literal("sqft")]), + t.Union([t.Literal("asc"), t.Literal("desc")]), + ]), + offset: t.Number({ default: 0 }), + limit: t.Number({ default: 16 }), + }) + ), } - }, - { - query: t.Partial(t.Object({ project: t.String() })), - } -); + ) + .get( + "/count", + async ({ query: { project, unitTypes, cost, floor, area, view } }) => { + try { + return ( + await db + .select({ count: count() }) + .from(unitsTable) + .where( + and( + project ? eq(unitsTable.project, project) : undefined, + unitTypes ? inArray(unitsTable.unitType, unitTypes) : undefined, + cost + ? between(unitsTable.salesPrice, cost[0], cost[1]) + : undefined, + floor + ? between(unitsTable.floor, floor[0], floor[1]) + : undefined, + area + ? between(unitsTable.squareFt, area[0], area[1]) + : undefined, + view ? eq(unitsTable.unitView, view) : undefined + ) + ) + )[0].count; + } catch (err) { + console.log((err as Error).message); + return error(500, "Internal server error"); + } + }, + { + query: t.Partial( + t.Object({ + project: t.String(), + unitTypes: t.Array( + t.Union([ + t.Literal("Studio Squared"), + t.Literal("1 BR Squared"), + t.Literal("Studio Flex"), + t.Literal("2 BR Squared"), + t.Literal("Studio2"), + t.Literal("One Bedroom2"), + t.Literal("One Bedroom Loft"), + t.Literal("Two Bedroom Loft"), + ]) + ), + cost: t.Tuple([t.Number(), t.Number()]), + floor: t.Tuple([t.Number(), t.Number()]), + area: t.Tuple([t.Number(), t.Number()]), + view: t.Union([ + t.Literal("Canal / Amenities"), + t.Literal("Corner-Canal / Amenities"), + t.Literal("Corner-Canal View"), + t.Literal("Business Bay"), + t.Literal("Park Facing"), + t.Literal("Corner-Park Facing"), + t.Literal("Partial Park"), + t.Literal("BK/DT / Amenities"), + t.Literal("Corner-BK/DT / Amenities"), + t.Literal("Corner-Canal / BK/DT View"), + t.Literal("Marina View"), + t.Literal("Partial Marina View"), + t.Literal("Partial Marina, Partial City View"), + t.Literal("City view"), + t.Literal("Marina View, Sea View"), + t.Literal("Partial Marina View, Partial Sea View"), + ]), + }) + ), + } + ) + .get( + "/filters/:filterName", + async ({ + query: { project, view, unitTypes, cost, floor, area }, + params: { filterName }, + }) => { + try { + const filters = and( + project ? eq(unitsTable.project, project) : undefined, + unitTypes && filterName !== "unitTypes" + ? inArray(unitsTable.unitType, unitTypes) + : undefined, + view && filterName !== "views" + ? eq(unitsTable.unitView, view) + : undefined, + cost && filterName !== "cost" + ? between(unitsTable.salesPrice, cost[0], cost[1]) + : undefined, + floor && filterName !== "floor" + ? between(unitsTable.floor, floor[0], floor[1]) + : undefined, + area && filterName !== "area" + ? between(unitsTable.squareFt, area[0], area[1]) + : undefined + ); + + if (filterName === "unitTypes") + return ( + await db + .selectDistinctOn([unitsTable.unitType], { + unitType: unitsTable.unitType, + }) + .from(unitsTable) + .where(filters) + ) + .map((unit) => unit.unitType) + .filter(Boolean); + else if (filterName === "views") + return ( + await db + .selectDistinctOn([unitsTable.unitView], { + unitView: unitsTable.unitView, + }) + .from(unitsTable) + .where(filters) + ) + .map((unit) => unit.unitView) + .filter(Boolean); + else if (filterName === "cost") + return ( + await db + .select({ + min: min(unitsTable.salesPrice), + max: max(unitsTable.salesPrice), + }) + .from(unitsTable) + .where(filters) + )[0]; + else if (filterName === "area") + return ( + await db + .select({ + min: min(unitsTable.squareFt), + max: max(unitsTable.squareFt), + }) + .from(unitsTable) + .where(filters) + )[0]; + return ( + await db + .select({ + min: min(unitsTable.floor), + max: max(unitsTable.floor), + }) + .from(unitsTable) + .where(filters) + )[0]; + } catch (err) { + console.log((err as Error).message); + return error(500, "Internal server error"); + } + }, + { + response: { + 200: t.Union([ + t.Array(t.String()), + t.Object({ + min: t.Number(), + max: t.Number(), + }), + ]), + 500: t.ObjectString({}), + }, + params: t.Object({ + filterName: t.Union([ + t.Literal("unitTypes"), + t.Literal("views"), + t.Literal("cost"), + t.Literal("area"), + t.Literal("floor"), + ]), + }), + query: t.Partial( + t.Object({ + project: t.String(), + unitTypes: t.Array( + t.Union([ + t.Literal("Studio Squared"), + t.Literal("1 BR Squared"), + t.Literal("Studio Flex"), + t.Literal("2 BR Squared"), + t.Literal("Studio2"), + t.Literal("One Bedroom2"), + t.Literal("One Bedroom Loft"), + t.Literal("Two Bedroom Loft"), + ]) + ), + cost: t.Tuple([t.Number(), t.Number()]), + floor: t.Tuple([t.Number(), t.Number()]), + area: t.Tuple([t.Number(), t.Number()]), + view: t.Union([ + t.Literal("Canal / Amenities"), + t.Literal("Corner-Canal / Amenities"), + t.Literal("Corner-Canal View"), + t.Literal("Business Bay"), + t.Literal("Park Facing"), + t.Literal("Corner-Park Facing"), + t.Literal("Partial Park"), + t.Literal("BK/DT / Amenities"), + t.Literal("Corner-BK/DT / Amenities"), + t.Literal("Corner-Canal / BK/DT View"), + t.Literal("Marina View"), + t.Literal("Partial Marina View"), + t.Literal("Partial Marina, Partial City View"), + t.Literal("City view"), + t.Literal("Marina View, Sea View"), + t.Literal("Partial Marina View, Partial Sea View"), + ]), + }) + ), + } + ); diff --git a/src/db/schema/units.ts b/src/db/schema/units.ts index 62883d6..c0179bf 100644 --- a/src/db/schema/units.ts +++ b/src/db/schema/units.ts @@ -1,37 +1,60 @@ -import { integer, pgTable, uuid, varchar, decimal } from "drizzle-orm/pg-core"; +import { primaryKey } from "drizzle-orm/pg-core"; +import { unique } from "drizzle-orm/pg-core"; +import { doublePrecision } from "drizzle-orm/pg-core"; +import { integer, pgTable, uuid, varchar } from "drizzle-orm/pg-core"; -export const unitsTable = pgTable("units", { - id: uuid("id").defaultRandom().primaryKey(), - unitNo: varchar("unit_no", { length: 10 }).notNull(), - number: integer("number").notNull(), - project: varchar("project", { length: 256 }).notNull(), - floor: integer("floor").notNull(), - unitType: varchar("unit_type", { - length: 256, - enum: ["Studio Squared", "1 BR Squared", "Studio Flex", "2 BR Squared"], - }).notNull(), - noOfBathrooms: integer("no_of_bathrooms").notNull(), - unitView: varchar("unit_view", { - length: 256, - enum: [ - "Canal / Amenities", - "Corner-Canal / Amenities", - "Corner-Canal View", - "Business Bay", - "Park Facing", - "Corner-Park Facing", - "Partial Park", - "BK/DT / Amenities", - "Corner-BK/DT / Amenities", - "Corner-Canal / BK/DT View", - ], - }).notNull(), - suitsArea: decimal("suits_area", { mode: "number" }).notNull(), - squareFt: decimal("square_ft", { mode: "number" }).notNull(), - noOfParkingSpace: integer("no_of_parking_space").notNull(), - salesPrice: integer("sales_price").notNull(), - state: varchar("state", { - length: 256, - enum: ["reserved", "sold_spa", "available", "sold_registered"], - }).notNull(), -}); +export const unitsTable = pgTable( + "units", + { + id: uuid("id").defaultRandom().primaryKey(), + unitNo: varchar("unit_no", { length: 10 }).notNull(), + number: integer("number"), + project: varchar("project", { length: 256 }).notNull(), + floor: integer("floor").notNull(), + unitType: varchar("unit_type", { + length: 256, + enum: [ + "Studio Squared", + "1 BR Squared", + "Studio Flex", + "2 BR Squared", + "One Bedroom2", + "Studio2", + "One Bedroom Loft", + "Two Bedroom Loft", + ], + }), + noOfBathrooms: integer("no_of_bathrooms").notNull(), + unitView: varchar("unit_view", { + length: 256, + enum: [ + "Canal / Amenities", + "Corner-Canal / Amenities", + "Corner-Canal View", + "Business Bay", + "Park Facing", + "Corner-Park Facing", + "Partial Park", + "BK/DT / Amenities", + "Corner-BK/DT / Amenities", + "Corner-Canal / BK/DT View", + "Marina View", + "Partial Marina View", + "Partial Marina, Partial City View", + "City view", + "Marina View, Sea View", + "Partial Marina View, Partial Sea View", + ], + }), + suitsArea: doublePrecision("suits_area").notNull(), + squareFt: doublePrecision("square_ft").notNull(), + noOfParkingSpace: integer("no_of_parking_space").notNull(), + salesPrice: integer("sales_price").notNull(), + state: varchar("state", { + length: 256, + enum: ["reserved", "sold_spa", "available", "sold_registered"], + }).notNull(), + balconyArea: doublePrecision("balcony_area").notNull(), + }, + (t) => [unique().on(t.project, t.unitNo)] +); diff --git a/src/index.ts b/src/index.ts index 6b91673..d789231 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,11 @@ import { Elysia } from "elysia"; import { unitsController } from "./controllers/units"; +import { cors } from "@elysiajs/cors"; -const app = new Elysia().use(unitsController).listen(3000); +const app = new Elysia() + .use(cors({ origin: "*" })) + .use(unitsController) + .listen(3000); console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` diff --git a/src/types/IUnit.ts b/src/types/IUnit.ts index 75eba40..d934636 100644 --- a/src/types/IUnit.ts +++ b/src/types/IUnit.ts @@ -2,12 +2,13 @@ export interface IUnit { unit_no: string; project: string; floor: string; - unit_type: string; + unit_type: string | false; + unit_view: string | false; no_of_bathrooms: number; - unit_view: string; suits_area: number; square_ft: number; no_of_parking_space: number; sales_price: number; state: string; + balcony_area: number; }