diff --git a/apps/api/.env-example b/apps/api/.env-example index 5199fc16..74ef6f3d 100644 --- a/apps/api/.env-example +++ b/apps/api/.env-example @@ -8,4 +8,7 @@ COOKIE_PASSWORD=blablablblablablblablablblablabl GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= +APP_URL=http://app.devfaq.localhost:3000 +REVALIDATION_TOKEN= + DATABASE_URL=postgres://postgres:api2022@localhost:54421/database_development diff --git a/apps/api/config/config.ts b/apps/api/config/config.ts index cd232ffe..122b226c 100644 --- a/apps/api/config/config.ts +++ b/apps/api/config/config.ts @@ -3,6 +3,8 @@ export function getConfig(name: "NODE_ENV"): "production" | "development"; export function getConfig(name: "ENV"): "production" | "staging" | "development" | "test"; export function getConfig(name: "GITHUB_CLIENT_ID"): string; export function getConfig(name: "GITHUB_CLIENT_SECRET"): string; +export function getConfig(name: "APP_URL"): string; +export function getConfig(name: "REVALIDATION_TOKEN"): string; export function getConfig(name: "GIT_BRANCH"): string; export function getConfig(name: "GIT_COMMIT_HASH"): string; export function getConfig(name: "VERSION"): string; @@ -21,6 +23,10 @@ export function getConfig(name: string): string | number { case "GITHUB_CLIENT_ID": case "GITHUB_CLIENT_SECRET": return val || ""; + case "APP_URL": + return val || ""; + case "REVALIDATION_TOKEN": + return val || ""; case "GIT_BRANCH": return val || "(unknown_branch)"; case "GIT_COMMIT_HASH": diff --git a/apps/api/modules/answers/answers.routes.ts b/apps/api/modules/answers/answers.routes.ts index 25510a65..762cd870 100644 --- a/apps/api/modules/answers/answers.routes.ts +++ b/apps/api/modules/answers/answers.routes.ts @@ -1,6 +1,7 @@ import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox"; import { Prisma } from "@prisma/client"; import { FastifyPluginAsync, preHandlerAsyncHookHandler, preHandlerHookHandler } from "fastify"; +import { revalidate } from "../../services/revalidation.service.js"; import { PrismaErrorCode } from "../db/prismaErrors.js"; import { isPrismaError } from "../db/prismaErrors.util.js"; import { dbAnswerToDto } from "./answers.mapper.js"; @@ -15,6 +16,7 @@ import { export const answerSelect = (userId: number) => { return { id: true, + questionId: true, content: true, sources: true, createdAt: true, @@ -34,6 +36,8 @@ export const answerSelect = (userId: number) => { } satisfies Prisma.QuestionAnswerSelect; }; +const revalidateQuestion = (id: number) => revalidate(`/questions/p/${id}`); + const answersPlugin: FastifyPluginAsync = async (fastify) => { const checkAnswerUserHook: preHandlerAsyncHookHandler = async (request) => { const { @@ -101,6 +105,8 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => { select: answerSelect(request.session.data?._user.id || 0), }); + await revalidateQuestion(id); + return { data: dbAnswerToDto(answer) }; } catch (err) { if (isPrismaError(err) && err.code === PrismaErrorCode.UniqueKeyViolation) { @@ -132,6 +138,8 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => { select: answerSelect(request.session.data?._user.id || 0), }); + await revalidateQuestion(answer.questionId); + return { data: dbAnswerToDto(answer) }; }, }); @@ -146,10 +154,12 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => { params: { id }, } = request; - await fastify.db.questionAnswer.delete({ + const { questionId } = await fastify.db.questionAnswer.delete({ where: { id }, }); + await revalidateQuestion(questionId); + return reply.status(204).send(); }, }); diff --git a/apps/api/services/revalidation.service.ts b/apps/api/services/revalidation.service.ts new file mode 100644 index 00000000..e47a009d --- /dev/null +++ b/apps/api/services/revalidation.service.ts @@ -0,0 +1,9 @@ +import { getConfig } from "../config/config.js"; + +export const revalidate = (path: string) => + fetch( + `${getConfig("APP_URL")}/api/revalidation?token=${getConfig( + "REVALIDATION_TOKEN", + )}&path=${path}`, + { method: "POST" }, + ); diff --git a/apps/api/typings/undici/index.d.ts b/apps/api/typings/undici/index.d.ts new file mode 100644 index 00000000..466bde1f --- /dev/null +++ b/apps/api/typings/undici/index.d.ts @@ -0,0 +1,9 @@ +/* eslint-disable import/export */ +// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/60924#issuecomment-1358837996 + +declare global { + export const { fetch, FormData, Headers, Request, Response }: typeof import("undici"); + export type { FormData, Headers, Request, RequestInit, Response } from "undici"; +} + +export {}; diff --git a/apps/app/.env.local-example b/apps/app/.env.local-example index 9ea860c3..f8e91e76 100644 --- a/apps/app/.env.local-example +++ b/apps/app/.env.local-example @@ -1,4 +1,5 @@ NEXT_PUBLIC_API_URL=http://api.devfaq.localhost:3002 NEXT_PUBLIC_APP_URL=http://app.devfaq.localhost:3000 +REVALIDATION_TOKEN= FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY= diff --git a/apps/app/src/app/(main-layout)/questions/p/[questionId]/page.tsx b/apps/app/src/app/(main-layout)/questions/p/[questionId]/page.tsx index beae24d2..ee54340e 100644 --- a/apps/app/src/app/(main-layout)/questions/p/[questionId]/page.tsx +++ b/apps/app/src/app/(main-layout)/questions/p/[questionId]/page.tsx @@ -42,3 +42,7 @@ export default async function SingleQuestionPage({ params }: { params: Params<"q ); } + +export async function generateStaticParams() { + return []; +} diff --git a/apps/app/src/components/QuestionAnswers/EditAnswer.tsx b/apps/app/src/components/QuestionAnswers/EditAnswer.tsx index a1834d28..ebf64018 100644 --- a/apps/app/src/components/QuestionAnswers/EditAnswer.tsx +++ b/apps/app/src/components/QuestionAnswers/EditAnswer.tsx @@ -33,10 +33,10 @@ export const EditAnswer = ({ deleteQuestionAnswerMutation.mutate( { id }, { + onSuccess: () => router.refresh(), onError: () => setIsError(true), }, ); - router.refresh(); }; if (isEditMode) { diff --git a/apps/app/src/lib/revalidation.ts b/apps/app/src/lib/revalidation.ts new file mode 100644 index 00000000..93372222 --- /dev/null +++ b/apps/app/src/lib/revalidation.ts @@ -0,0 +1,3 @@ +export const validatePath = (path: string | string[] | undefined): path is string => { + return Boolean(path) && typeof path === "string"; +}; diff --git a/apps/app/src/pages/api/revalidation.ts b/apps/app/src/pages/api/revalidation.ts new file mode 100644 index 00000000..45a84ce7 --- /dev/null +++ b/apps/app/src/pages/api/revalidation.ts @@ -0,0 +1,21 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { validatePath } from "../../lib/revalidation"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { token, path } = req.query; + + if (token !== process.env.REVALIDATION_TOKEN) { + return res.status(401).json({ message: "Invalid token" }); + } + + if (!validatePath(path)) { + return res.status(400).json({ message: "Incorrect path format" }); + } + + try { + await res.revalidate(path); + return res.status(204).end(); + } catch (err) { + return res.status(500).json({ message: "Error revalidating" }); + } +}