feat(cache): implement a simple caching mechanism with TTL and prefix invalidation

refactor(expenses): integrate caching for periodic expenses and related operations
refactor(quotes): add caching for quotes retrieval and history endpoints
This commit is contained in:
Jose Selesan
2026-05-29 19:25:15 -03:00
parent 801f04df12
commit 988d58d761
9 changed files with 129 additions and 10 deletions

View File

@@ -1,5 +1,6 @@
import { z } from "zod";
import { prisma } from "../../lib/prisma";
import { cache } from "../../lib/cache";
import { PaymentStatus } from "../../generated/prisma/client";
export const createPeriodicExpenseSchema = z.object({
@@ -37,28 +38,38 @@ export function getCurrentYearMonthUTC(): { year: number; month: number } {
}
export async function listPeriodicExpenses() {
return prisma.periodicExpense.findMany({
const cached = cache.get("expenses:periodic");
if (cached) return cached;
const data = await prisma.periodicExpense.findMany({
where: { isDeleted: false },
orderBy: { description: "asc" },
});
cache.set("expenses:periodic", data);
return data;
}
export async function createPeriodicExpense(data: z.infer<typeof createPeriodicExpenseSchema>) {
const result = await prisma.periodicExpense.create({ data });
const { year, month } = getCurrentYearMonthUTC();
const currentMonthApplicable = data.periods.includes(month);
cache.invalidateByPrefix("expenses:");
return { ...result, currentMonthApplicable };
}
export async function updatePeriodicExpense(id: number, data: z.infer<typeof updatePeriodicExpenseSchema>) {
return prisma.periodicExpense.update({ where: { id }, data });
const result = await prisma.periodicExpense.update({ where: { id }, data });
cache.invalidateByPrefix("expenses:");
return result;
}
export async function softDeletePeriodicExpense(id: number) {
return prisma.periodicExpense.update({
const result = await prisma.periodicExpense.update({
where: { id },
data: { isDeleted: true },
});
cache.invalidateByPrefix("expenses:");
return result;
}
export async function generateMonthlyExpense(id: number) {
@@ -71,7 +82,7 @@ export async function generateMonthlyExpense(id: number) {
if (existing) return existing;
const dueDate = buildDueDate(year, month, periodic.defaultDueDay);
return prisma.expense.create({
const result = await prisma.expense.create({
data: {
description: periodic.description,
periodicExpenseId: id,
@@ -82,6 +93,8 @@ export async function generateMonthlyExpense(id: number) {
status: PaymentStatus.PENDING,
},
});
cache.invalidateByPrefix("expenses:");
return result;
}
export async function listExpenses(params: {
@@ -92,6 +105,11 @@ export async function listExpenses(params: {
search?: string;
}) {
const { status, page = 1, pageSize = 10, periodicExpenseId, search } = params;
const cacheKey = `expenses:list:${status ?? "all"}:${page}:${pageSize}:${periodicExpenseId ?? "none"}:${search ?? ""}`;
const cached = cache.get(cacheKey);
if (cached) return cached;
const where: Record<string, unknown> = {};
if (status === "PENDING" || status === "PAYED") {
where.status = status;
@@ -112,14 +130,16 @@ export async function listExpenses(params: {
}),
prisma.expense.count({ where }),
]);
return { data, total, page, pageSize };
const result = { data, total, page, pageSize };
cache.set(cacheKey, result);
return result;
}
export async function createNonPeriodicExpense(data: z.infer<typeof createNonPeriodicExpenseSchema>) {
const dueDate = new Date(data.dueDate);
const year = dueDate.getUTCFullYear();
const month = dueDate.getUTCMonth() + 1;
return prisma.expense.create({
const result = await prisma.expense.create({
data: {
description: data.description,
amount: data.amount,
@@ -131,11 +151,13 @@ export async function createNonPeriodicExpense(data: z.infer<typeof createNonPer
status: PaymentStatus.PAYED,
},
});
cache.invalidateByPrefix("expenses:");
return result;
}
export async function payExpense(id: number, data: z.infer<typeof payExpenseSchema>) {
const paymentDate = data.paymentDate ? new Date(data.paymentDate) : new Date();
return prisma.expense.update({
const result = await prisma.expense.update({
where: { id },
data: {
status: PaymentStatus.PAYED,
@@ -143,36 +165,55 @@ export async function payExpense(id: number, data: z.infer<typeof payExpenseSche
paymentDate,
},
});
cache.invalidateByPrefix("expenses:");
return result;
}
export async function getMonthlyPayedTotal(year: number, month: number) {
const cacheKey = `expenses:monthly-total:${year}:${month}`;
const cached = cache.get(cacheKey);
if (cached) return cached;
const result = await prisma.$queryRaw<Array<{ total: string | null }>>`
SELECT CAST(SUM("amountPayed") AS NUMERIC) as total
FROM expenses
WHERE year = ${year} AND month = ${month} AND status = ${PaymentStatus.PAYED}::"PaymentStatus"
`;
return { total: Number(result[0]?.total ?? 0) };
const data = { total: Number(result[0]?.total ?? 0) };
cache.set(cacheKey, data);
return data;
}
export async function getMonthlyTotals(year: number, month: number) {
const cacheKey = `expenses:totals:${year}:${month}`;
const cached = cache.get(cacheKey);
if (cached) return cached;
const result = await prisma.$queryRaw<Array<{ payed: string | null; pending: string | null }>>`
SELECT
(SELECT CAST(SUM("amountPayed") AS NUMERIC) FROM expenses WHERE year = ${year} AND month = ${month} AND status = ${PaymentStatus.PAYED}::"PaymentStatus") as payed,
(SELECT CAST(SUM(amount) AS NUMERIC) FROM expenses WHERE year = ${year} AND month = ${month} AND status = ${PaymentStatus.PENDING}::"PaymentStatus") as pending
`;
return {
const data = {
payed: Number(result[0]?.payed ?? 0),
pending: Number(result[0]?.pending ?? 0),
};
cache.set(cacheKey, data);
return data;
}
export async function getTotalPending() {
const cached = cache.get("expenses:pending-total");
if (cached) return cached;
const result = await prisma.$queryRaw<Array<{ total: string | null }>>`
SELECT CAST(SUM(amount) AS NUMERIC) as total
FROM expenses
WHERE status = ${PaymentStatus.PENDING}::"PaymentStatus"
`;
return { total: Number(result[0]?.total ?? 0) };
const data = { total: Number(result[0]?.total ?? 0) };
cache.set("expenses:pending-total", data);
return data;
}
export async function generateExpensesForCurrentMonth() {
@@ -205,5 +246,6 @@ export async function generateExpensesForCurrentMonth() {
});
results.push({ id: periodic.id, status: "created" });
}
cache.invalidateByPrefix("expenses:");
return results;
}