feat: add revalidation functionality and integrate it into various components and API routes

This commit is contained in:
Marco Pedone 2025-10-17 19:14:27 +02:00
parent bc1f114837
commit 727f1e760c
10 changed files with 331 additions and 106 deletions

View file

@ -28,6 +28,7 @@ env:
SKIP_REDIS: "false"
SKEBBY_USER: "test"
SKEBBY_PASS : "test"
REVALIDATION_SECRET: "test"
jobs:
build:
runs-on: ubuntu-latest

View file

@ -63,6 +63,7 @@ ENV KEYDB_URL="localhost:6380"
ENV TILES_URL="localhost:6379"
ENV SKEBBY_USER="mock"
ENV SKEBBY_PASS="mock"
ENV REVALIDATION_SECRET="mock"
RUN SKIP_ENV_VALIDATION=1 npm run build
@ -128,6 +129,8 @@ ARG SKEBBY_USER
ENV SKEBBY_USER=$SKEBBY_USER
ARG SKEBBY_PASS
ENV SKEBBY_PASS=$SKEBBY_PASS
ENV REVALIDATION_SECRET="SWKpgaaLfsyeqV1eOT0WG7TUFBewir8kJXure3O37ki8Lt4Z4IiEgBW4zPHDdM5c"
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

View file

@ -30,6 +30,7 @@ const server = z.object({
TILES_URL: z.string(),
SKEBBY_USER: z.string(),
SKEBBY_PASS: z.string(),
REVALIDATION_SECRET: z.string(),
});
/**
@ -73,6 +74,7 @@ const processEnv = {
TILES_URL: process.env.TILES_URL,
SKEBBY_USER: process.env.SKEBBY_USER,
SKEBBY_PASS: process.env.SKEBBY_PASS,
REVALIDATION_SECRET: process.env.REVALIDATION_SECRET,
};
// Don't touch the part below

View file

@ -2,8 +2,10 @@ import { format } from "date-fns";
import { it } from "date-fns/locale";
import {
CalendarIcon,
CodeSquare,
Eye,
Printer,
RefreshCcw,
RotateCw,
TriangleAlert,
} from "lucide-react";
@ -132,9 +134,15 @@ export const AnnuncioEditForm = ({ data }: { data: Annunci }) => {
const { mutateAsync: revalidate } =
api.settings.revalidateAnnuncio.useMutation({
onSettled: async () => {
toast.success("Revalidazione completata");
},
});
const { mutateAsync: update } = api.settings.updateAnnuncio.useMutation({
onSettled: async () => {
toast.success(
"La revalidazione dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.",
"L'aggiornamento dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.",
{
duration: 5000,
},
@ -214,11 +222,20 @@ export const AnnuncioEditForm = ({ data }: { data: Annunci }) => {
onSubmit={form.handleSubmit(onSubmit)}
>
<div className="mx-auto grid w-full max-w-7xl flex-1 auto-rows-max gap-4">
<div className="flex flex-col gap-2">
<div className="flex flex-wrap items-center gap-2">
<h1 className="whitespace-nowrap font-semibold text-2xl tracking-tight">
Annuncio {data.codice}
</h1>
<StatusBadge status={data.stato} />
<div className="ml-auto flex items-center gap-2">
<Button className="bg-green-500" size="sm" type="submit">
Salva
</Button>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Link href={`/annuncio/${data.codice}`} target="_blank">
<Button
className="flex items-center gap-2"
@ -242,12 +259,35 @@ export const AnnuncioEditForm = ({ data }: { data: Annunci }) => {
<Printer /> Scheda
</Button>
</Link>
<Confirm
description="Sei sicuro di voler richiedere la revalidazione dell'annuncio? Questa operazione potrebbe richiedere alcuni minuti prima che le modifiche siano visibili e resetterà eventuali modifiche effettuate dopo la sincronizzazione giornaliera."
onConfirm={async () => {
await revalidate({ cod: data.codice });
<Popover>
<PopoverTrigger asChild>
<Button
className="flex items-center gap-2"
size="sm"
type="button"
>
<CodeSquare /> Strumenti
</Button>
</PopoverTrigger>
<PopoverContent className="flex w-fit items-center gap-2">
<Button
className="flex items-center gap-2 bg-purple-500"
onClick={async () => {
await revalidate({
cod: data.codice,
});
}}
title="Revalidazione Annuncio"
size="sm"
type="button"
>
<RefreshCcw /> Rigenera pagina
</Button>
<Confirm
description="Sei sicuro di voler richiedere l'aggiornamento dell'annuncio? Questa operazione potrebbe richiedere alcuni minuti prima che le modifiche siano visibili e resetterà eventuali modifiche effettuate dopo la sincronizzazione giornaliera."
onConfirm={async () => {
await update({ cod: data.codice });
}}
title="Aggiornamento Annuncio"
>
<Button
className="flex items-center gap-2"
@ -255,16 +295,11 @@ export const AnnuncioEditForm = ({ data }: { data: Annunci }) => {
type="button"
variant="warning"
>
<TriangleAlert /> Rivalida
<TriangleAlert /> Aggiorna
</Button>
</Confirm>
<div className="flex items-center gap-2 md:ml-auto">
<Button size="sm" type="button" variant="outline">
Annulla
</Button>
<Button className="bg-green-500" size="sm" type="submit">
Salva
</Button>
</PopoverContent>
</Popover>
</div>
</div>
<div className="grid h-max gap-3 md:grid-cols-[1fr_250px] lg:grid-cols-3">

View file

@ -236,7 +236,7 @@ export async function getStaticProps(
meta,
trpcState: ssg.dehydrate(),
},
revalidate: 1,
revalidate: 21600, // 6 hours - ensures fresh data 4x per day
};
}

View file

@ -0,0 +1,31 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { env } from "~/env.mjs";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.query.secret !== env.REVALIDATION_SECRET) {
return res.status(401).json({ message: "Invalid token" });
}
const path = req.query.path as string;
if (!path) {
return res.status(400).json({ message: "Missing path parameter" });
}
try {
await res.revalidate(`${path}`);
return res.json({
success: true,
message: `Page ${path} revalidated successfully`,
});
} catch (err) {
console.error("Error revalidating:", err);
return res.status(500).json({
success: false,
message: err instanceof Error ? err.message : "Unknown error",
});
}
}

View file

@ -1,4 +1,4 @@
import { RefreshCcw, TriangleAlert } from "lucide-react";
import { CodeSquare, RefreshCcw, TriangleAlert } from "lucide-react";
import toast from "react-hot-toast";
import { Confirm } from "~/components/confirm";
import { AreaRiservataLayout } from "~/components/Layout";
@ -6,6 +6,11 @@ import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page";
import { AnnunciTable } from "~/components/tables/annunci-tables";
import { Button } from "~/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import type { NextPageWithLayout } from "~/pages/_app";
import { generateSSGHelper } from "~/server/utils/ssgHelper";
import { api } from "~/utils/api";
@ -13,8 +18,7 @@ import { api } from "~/utils/api";
const Admin_Annunci: NextPageWithLayout = () => {
const { data, isLoading, refetch } = api.annunci.getAnnunciList.useQuery();
const { mutateAsync: revalidate } =
api.settings.revalidateAllAnnunci.useMutation({
const { mutateAsync: update } = api.settings.updateAllAnnunci.useMutation({
onSettled: async () => {
toast.success(
"La revalidazione dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.",
@ -24,6 +28,12 @@ const Admin_Annunci: NextPageWithLayout = () => {
);
},
});
const { mutateAsync: revalidate } =
api.settings.revalidateAllAnnunci.useMutation({
onSettled: async () => {
toast.success("Revalidazione completata");
},
});
if (isLoading) return <LoadingPage />;
if (!data) return <Status500 />;
return (
@ -39,12 +49,33 @@ const Admin_Annunci: NextPageWithLayout = () => {
>
<RefreshCcw className="size-6" />
</button>
<Confirm
description="Sei sicuro di voler richiedere la revalidazione gli annunci? Questa operazione potrebbe richiedere alcuni minuti prima che le modifiche siano visibili e resetterà eventuali modifiche effettuate dopo la sincronizzazione giornaliera."
onConfirm={async () => {
<Popover>
<PopoverTrigger asChild>
<Button
className="flex items-center gap-2"
size="sm"
type="button"
>
<CodeSquare /> Strumenti avanzati
</Button>
</PopoverTrigger>
<PopoverContent className="flex flex-col items-center gap-2">
<Button
className="flex items-center gap-2 bg-purple-500"
onClick={async () => {
await revalidate();
}}
title="Rivalida tutti gli annunci"
size="sm"
type="button"
>
<RefreshCcw /> Rigenera pagine annnunci
</Button>
<Confirm
description="Sei sicuro di voler richiedere l'aggiornamento degli annunci? Questa operazione potrebbe richiedere alcuni minuti prima che le modifiche siano visibili e resetterà eventuali modifiche effettuate dopo la sincronizzazione giornaliera."
onConfirm={async () => {
await update();
}}
title="Aggiornamento Annunci"
>
<Button
className="flex items-center gap-2"
@ -52,9 +83,11 @@ const Admin_Annunci: NextPageWithLayout = () => {
type="button"
variant="warning"
>
<TriangleAlert /> Rivalida tutti gli annunci
<TriangleAlert /> Aggiorna tutti gli annunci
</Button>
</Confirm>
</PopoverContent>
</Popover>
</div>
</div>
<AnnunciTable data={data} />

View file

@ -7,6 +7,7 @@ import {
createTRPCRouter,
publicProcedure,
} from "~/server/api/trpc";
import { getCodici_AnnunciHandler } from "~/server/controllers/annunci.controller";
import {
addEtichetta,
getAllEtichette,
@ -36,6 +37,10 @@ import {
newStringa,
updateStringhe,
} from "~/server/services/testi_stringhe.service";
import {
revalidate,
revalidateMultiple,
} from "~/server/utils/revalidationHelper";
import {
zEtichettaId,
zFlagsId,
@ -250,22 +255,20 @@ export const settingsRouter = createTRPCRouter({
stringaValue: input.stringaValue,
});
}),
revalidateAllAnnunci: adminProcedure.mutation(async () => {
try {
const response = await fetch(`${env.BACKENDSERVER_URL}/update`);
if (!response.ok) {
const errorData = await response.text().catch(() => "Unknown error");
throw new Error(
`Revalidation failed with status ${response.status}: ${errorData}`,
const annunciOnline = await getCodici_AnnunciHandler();
const { success, failed } = await revalidateMultiple(
annunciOnline.map((cod) => `/annuncio/${cod}`),
);
console.log(`Successfully revalidated pages: ${success.join(", ")}`);
if (failed.length > 0) {
failed.forEach(({ path, error }) => {
console.error(`Failed to revalidate ${path}: ${error}`);
});
}
const data = await response.json().catch(() => ({}));
return {
status: "success",
details: data,
};
} catch (err) {
console.error("Revalidation error:", err);
@ -284,21 +287,9 @@ export const settingsRouter = createTRPCRouter({
)
.mutation(async ({ input }) => {
try {
const response = await fetch(
`${env.BACKENDSERVER_URL}/update-cod/${input.cod}`,
);
if (!response.ok) {
const errorData = await response.text().catch(() => "Unknown error");
throw new Error(
`Revalidation failed with status ${response.status}: ${errorData}`,
);
}
const data = await response.json().catch(() => ({}));
await revalidate(`/annuncio/${input.cod}`);
return {
status: "success",
details: data,
};
} catch (err) {
console.error("Revalidation error:", err);
@ -309,4 +300,74 @@ export const settingsRouter = createTRPCRouter({
});
}
}),
updateAllAnnunci: adminProcedure.mutation(async () => {
try {
const response = await fetch(`${env.BACKENDSERVER_URL}/update`);
if (!response.ok) {
const errorData = await response.text().catch(() => "Unknown error");
throw new Error(
`Update failed with status ${response.status}: ${errorData}`,
);
}
const data = await response.json().catch(() => ({}));
const annunciOnline = await getCodici_AnnunciHandler();
const { success, failed } = await revalidateMultiple(
annunciOnline.map((cod) => `/annuncio/${cod}`),
);
console.log(`Successfully revalidated pages: ${success.join(", ")}`);
if (failed.length > 0) {
failed.forEach(({ path, error }) => {
console.error(`Failed to revalidate ${path}: ${error}`);
});
}
return {
status: "success",
details: data,
};
} catch (err) {
console.error("Update error:", err);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: err instanceof Error ? err.message : "Update failed",
cause: err,
});
}
}),
updateAnnuncio: adminProcedure
.input(
z.object({
cod: z.string(),
}),
)
.mutation(async ({ input }) => {
try {
const response = await fetch(
`${env.BACKENDSERVER_URL}/update-cod/${input.cod}`,
);
if (!response.ok) {
const errorData = await response.text().catch(() => "Unknown error");
throw new Error(
`Update failed with status ${response.status}: ${errorData}`,
);
}
const data = await response.json().catch(() => ({}));
await revalidate(`/annuncio/${input.cod}`);
return {
status: "success",
details: data,
};
} catch (err) {
console.error("Update error:", err);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: err instanceof Error ? err.message : "Update failed",
cause: err,
});
}
}),
});

View file

@ -9,6 +9,7 @@ import type { ServizioServizioId } from "~/schemas/public/Servizio";
import { db } from "~/server/db";
import { AnnuncioObjectWithImages } from "~/server/services/annunci.service";
import { createSrcset } from "~/server/services/imageServer";
import { revalidate } from "../utils/revalidationHelper";
// const ratelimit = new RateLimiterHandler({
// windowSize: 10,
@ -371,13 +372,15 @@ export const editAnnuncioHandler = async ({
data: AnnunciUpdate;
}) => {
try {
await db
const annuncio = await db
.updateTable("annunci")
.set({
...data,
})
.where("id", "=", annuncioId)
.execute();
.returning("codice")
.executeTakeFirstOrThrow();
await revalidate(`/annuncio/${annuncio.codice}`);
return true;
} catch (e) {
throw new TRPCError({

View file

@ -0,0 +1,56 @@
export const revalidate = async (path: string) => {
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/revalidation?secret=${process.env.REVALIDATION_SECRET}&path=${encodeURIComponent(path)}`,
{
method: "GET",
},
);
if (!res.ok) {
const errorData = (await res.json()) as {
message: string;
success: boolean;
};
throw new Error(
`Failed to revalidate path ${path}: ${errorData.message}`,
);
}
} catch (error) {
console.error(`Error revalidating path ${path}:`, error);
throw error;
}
};
export const revalidateMultiple = async (
paths: string[],
): Promise<{
success: string[];
failed: Array<{ path: string; error: string }>;
}> => {
const results = await Promise.allSettled(
paths.map((path) => revalidate(path)),
);
const success: string[] = [];
const failed: Array<{ path: string; error: string }> = [];
results.forEach((result, index) => {
const path = paths[index];
if (!path) {
return;
}
if (result.status === "fulfilled") {
success.push(path);
} else {
failed.push({
path,
error:
result.reason instanceof Error
? result.reason.message
: "Unknown error",
});
}
});
return { success, failed };
};