Skip to content

Commit

Permalink
Fix no results when querying API
Browse files Browse the repository at this point in the history
  • Loading branch information
ruchernchong committed Dec 23, 2024
1 parent e83ba61 commit 8a4935e
Show file tree
Hide file tree
Showing 11 changed files with 628 additions and 84 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"dev": "sst dev --stage dev",
"test": "vitest",
"test:coverage": "vitest --coverage",
"deploy": "sst deploy",
"remove": "sst remove",
"console": "sst console",
Expand All @@ -24,6 +25,7 @@
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/node": "^20.12.7",
"@vitest/coverage-v8": "2.1.5",
"drizzle-kit": "^0.30.1",
"typescript": "^5.4.5",
"vitest": "^2.1.5"
Expand Down
462 changes: 462 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

81 changes: 40 additions & 41 deletions src/lib/getCarsByFuelType.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import db from "@/config/db";
import { getLatestMonth } from "@/lib/getLatestMonth";
import { cars } from "@/schema";
import { FuelType } from "@/types";
import { format, subMonths } from "date-fns";
import { and, asc, desc, gte, ilike, or } from "drizzle-orm";
import type { FuelType } from "@/types";
import getTrailingTwelveMonths from "@/utils/getTrailingTwelveMonths";
import { and, asc, between, desc, eq, ilike, or } from "drizzle-orm";

const HYBRID_TYPES = [
"Diesel-Electric",
Expand All @@ -11,49 +12,47 @@ const HYBRID_TYPES = [
"Petrol-Electric (Plug-In)",
];

const FUEL_TYPE_MAP = {
DIESEL: [FuelType.Diesel],
ELECTRIC: [FuelType.Electric],
OTHERS: [FuelType.Others],
PETROL: [FuelType.Petrol],
};

const trailingTwelveMonths = format(subMonths(new Date(), 12), "yyyy-MM");

export const getCarsByFuelType = async (fuelType: string, month?: string) => {
const normalisedFuelType = fuelType.toUpperCase();
export const getCarsByFuelType = async (fuelType: FuelType, month?: string) => {
const latestMonth = await getLatestMonth(cars);

const filters = [
fuelType &&
or(
ilike(cars.fuelType, FUEL_TYPE_MAP[normalisedFuelType]),
...HYBRID_TYPES.map((type) => ilike(cars.fuelType, type)),
ilike(cars.fuel_type, fuelType),
...HYBRID_TYPES.map((type) => ilike(cars.fuel_type, type)),
),
month && gte(cars.month, trailingTwelveMonths),
month
? eq(cars.month, month)
: between(cars.month, getTrailingTwelveMonths(latestMonth), latestMonth),
];

const result = await db
.select()
.from(cars)
.where(and(...filters))
.orderBy(desc(cars.month), asc(cars.make));

return result.reduce((result, { month, make, number, ...car }) => {
const existingCar = result.find(
(car) => car.month === month && car.make === make,
);

if (existingCar) {
existingCar.number += Number(number);
} else {
result.push({
...car,
month,
make,
number: Number(number),
});
}

return result;
}, []);
try {
const results = await db
.select()
.from(cars)
.where(and(...filters))
.orderBy(desc(cars.month), asc(cars.make));

return results.reduce((result, { month, make, number, ...car }) => {
const existingCar = result.find(
(car) => car.month === month && car.make === make,
);

if (existingCar) {
existingCar.number += Number(number);
} else {
result.push({
...car,
month,
make,
number: Number(number),
});
}

return result;
}, []);
} catch (e) {
console.error(e);
throw e;
}
};
10 changes: 6 additions & 4 deletions src/lib/getLatestMonth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ import db from "@/config/db";
import { desc, max } from "drizzle-orm";
import type { PgTable } from "drizzle-orm/pg-core";

export const getLatestMonth = async <T extends PgTable>(table: T) => {
export const getLatestMonth = async <T extends PgTable>(
table: T,
): Promise<string> => {
const key = "month";

try {
const result = await db
const results = await db
.select({ month: max(table[key]) })
.from(table)
.orderBy(desc(max(table[key])))
.limit(1);

if (!result) {
if (!results) {
throw new Error(`No data found for table: ${table}`);
}

return result[0].month;
return results[0].month;
} catch (e) {
console.error(e);
throw e;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/getUniqueMonths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ export const getUniqueMonths = async <T extends PgTable>(
let months = await redis.smembers(CACHE_KEY);

if (months.length === 0) {
const result = await db
const results = await db
.selectDistinct({ month: table[key] })
.from(table)
.orderBy(desc(table[key]));

months = result.map(({ month }) => month);
months = results.map(({ month }) => month);

await redis.sadd(CACHE_KEY, ...months);
await redis.expire(CACHE_KEY, CACHE_TTL);
Expand Down
37 changes: 37 additions & 0 deletions src/utils/__tests__/getTrailingTwelveMonths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import getTrailingTwelveMonths from "@/utils/getTrailingTwelveMonths";
import { describe, expect, it } from "vitest";

describe("getTrailingTwelveMonths", () => {
// Basic functionality tests
describe("Standard date inputs", () => {
it("should return correct trailing 12 months start date for mid-year input", () => {
expect(getTrailingTwelveMonths("2023-07")).toBe("2022-08");
});

it("should return correct trailing 12 months start date for year-end", () => {
expect(getTrailingTwelveMonths("2023-12")).toBe("2023-01");
});

it("should return correct trailing 12 months start date for year-start", () => {
expect(getTrailingTwelveMonths("2023-01")).toBe("2022-02");
});
});

// Edge case tests
describe("Edge cases", () => {
it("should handle single-digit month inputs", () => {
expect(getTrailingTwelveMonths("2023-03")).toBe("2022-04");
});

it("should handle year transitions correctly", () => {
expect(getTrailingTwelveMonths("2024-01")).toBe("2023-02");
});
});

// Leap year considerations
describe("Leap year handling", () => {
it("should work correctly across leap year boundaries", () => {
expect(getTrailingTwelveMonths("2024-02")).toBe("2023-03");
});
});
});
8 changes: 8 additions & 0 deletions src/utils/getTrailingTwelveMonths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { format, subMonths } from "date-fns";

const getTrailingTwelveMonths = (dateString: string) => {
const targetDate = new Date(`${dateString}-01`);
return format(subMonths(targetDate, 11), "yyyy-MM");
};

export default getTrailingTwelveMonths;
81 changes: 47 additions & 34 deletions src/v1/routes/cars.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,63 @@
import db from "@/config/db";
import redis from "@/config/redis";
import { getLatestMonth } from "@/lib/getLatestMonth";
import { getUniqueMonths } from "@/lib/getUniqueMonths";
import { groupMonthsByYear } from "@/lib/groupMonthsByYear";
import { cars } from "@/schema";
import type { Make } from "@/types";
import { and, asc, between, desc, ilike } from "drizzle-orm";
import getTrailingTwelveMonths from "@/utils/getTrailingTwelveMonths";
import { and, asc, between, desc, eq, ilike } from "drizzle-orm";
import { Hono } from "hono";

const app = new Hono();

app.get("/", async (c) => {
const query = c.req.query();

const cacheKey = `cars:${JSON.stringify(query)}`;

const cachedData = await redis.get(cacheKey);
if (cachedData) {
return c.json(cachedData);
}

const today = new Date();
const pastYear = new Date(today.getFullYear() - 1, today.getMonth() + 1, 1);
const pastYearFormatted = pastYear.toISOString().slice(0, 7); // YYYY-MM format
const currentMonthFormatted = today.toISOString().slice(0, 7); // YYYY-MM format

const conditions = [
...(query.month
? []
: [between(cars.month, pastYearFormatted, currentMonthFormatted)]),
];

for (const [key, value] of Object.entries(query)) {
if (!value) continue;

conditions.push(ilike(cars[key], `%${value}%`));
const { month, ...queries } = query;

// const CACHE_KEY = `cars:${JSON.stringify(query)}`;
//
// const cachedData = await redis.get(CACHE_KEY);
// if (cachedData) {
// return c.json(cachedData);
// }

try {
const latestMonth = !month && (await getLatestMonth(cars));

const filters = [
month
? eq(cars.month, month)
: between(
cars.month,
getTrailingTwelveMonths(latestMonth),
latestMonth,
),
];

for (const [key, value] of Object.entries(queries)) {
filters.push(ilike(cars[key], value.split("-").join("%")));
}

const results = await db
.select()
.from(cars)
.where(and(...filters))
.orderBy(desc(cars.month));

// await redis.set(CACHE_KEY, JSON.stringify(results), { ex: 86400 });

return c.json(results);
} catch (e) {
console.error("Car query error:", e);
return c.json(
{
error: "An error occurred while fetching cars",
details: e.message,
},
500,
);
}

const response = await db
.select()
.from(cars)
.where(and(...conditions))
.orderBy(desc(cars.month));

await redis.set(cacheKey, JSON.stringify(response), { ex: 86400 });

return c.json(response);
});

app.get("/months", async (c) => {
Expand Down
4 changes: 2 additions & 2 deletions src/v1/routes/coe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ app.get("/", async (c) => {
to && lte(coe.month, to),
];

const result = await db
const results = await db
.select()
.from(coe)
.where(and(...filters))
Expand Down Expand Up @@ -64,7 +64,7 @@ app.get("/latest", async (c) => {
}

const latestMonth = await getLatestMonth(coe);
const result = await db
const results = await db
.select()
.from(coe)
.where(eq(coe.month, latestMonth))
Expand Down
2 changes: 1 addition & 1 deletion src/v1/routes/makes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ app.get("/:make", async (c) => {
vehicle_type && ilike(cars.vehicle_type, vehicle_type.split("-").join("%")),
].filter(Boolean);

const result = await db
const results = await db
.select()
.from(cars)
.where(and(...filters))
Expand Down
21 changes: 21 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { resolve } from "node:path";
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
globals: true,
environment: "node",
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
include: ["src/**/*.{js,ts}"],
exclude: ["**/node_modules/**", "**/*.d.ts"],
},
include: ["src/**/*.{test,spec}.{js,ts}"],
},
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
},
},
});

0 comments on commit 8a4935e

Please sign in to comment.