From 6b903229213fe5d217bcd80424558b03f197d265 Mon Sep 17 00:00:00 2001 From: adipol1359 Date: Sun, 8 Jan 2023 18:05:09 +0100 Subject: [PATCH 1/5] feat(app): add static rendering to single question --- apps/app/.env.local-example | 1 + .../questions/p/[questionId]/page.tsx | 4 ++++ .../AddAnswerForm/AddAnswerForm.tsx | 18 ++++++++++++---- apps/app/src/hooks/useRevalidation.ts | 8 +++++++ apps/app/src/lib/revalidation.ts | 3 +++ apps/app/src/pages/api/revalidation.ts | 21 +++++++++++++++++++ apps/app/src/services/revalidation.service.ts | 5 +++++ 7 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 apps/app/src/hooks/useRevalidation.ts create mode 100644 apps/app/src/lib/revalidation.ts create mode 100644 apps/app/src/pages/api/revalidation.ts create mode 100644 apps/app/src/services/revalidation.service.ts diff --git a/apps/app/.env.local-example b/apps/app/.env.local-example index 9ea860c3..f1c16b8e 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 +NEXT_PUBLIC_REVALIDATION_SECRET= 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 80d84a63..abb8aff0 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/AddAnswerForm/AddAnswerForm.tsx b/apps/app/src/components/QuestionAnswers/AddAnswerForm/AddAnswerForm.tsx index 32f82971..31bf3465 100644 --- a/apps/app/src/components/QuestionAnswers/AddAnswerForm/AddAnswerForm.tsx +++ b/apps/app/src/components/QuestionAnswers/AddAnswerForm/AddAnswerForm.tsx @@ -7,6 +7,7 @@ import { Button } from "../../Button/Button"; import { WysiwygEditor } from "../../WysiwygEditor/WysiwygEditor"; import { URL_REGEX } from "../../../lib/constants"; import { Error } from "../../Error"; +import { useRevalidation } from "../../../hooks/useRevalidation"; import { AnswerSources } from "./AnswerSources"; type AddAnswerFormProps = Readonly<{ @@ -21,9 +22,13 @@ export const AddAnswerForm = ({ questionId }: AddAnswerFormProps) => { const [isError, setIsError] = useState(false); const { createQuestionAnswerMutation } = useQuestionMutation(); + const { revalidateMutation } = useRevalidation(); const disabled = - content.trim().length === 0 || !sources.every((source) => URL_REGEX.test(source)); + content.trim().length === 0 || + !sources.every((source) => URL_REGEX.test(source)) || + createQuestionAnswerMutation.isLoading || + revalidateMutation.isLoading; const handleFormSubmit = (event: FormEvent) => { event.preventDefault(); @@ -36,9 +41,14 @@ export const AddAnswerForm = ({ questionId }: AddAnswerFormProps) => { }, { onSuccess: () => { - router.refresh(); - setContent(""); - setSources([]); + revalidateMutation.mutate(`/questions/p/${questionId}`, { + onSuccess: () => { + router.refresh(); + setContent(""); + setSources([]); + }, + onError: () => setIsError(true), + }); }, onError: () => setIsError(true), }, diff --git a/apps/app/src/hooks/useRevalidation.ts b/apps/app/src/hooks/useRevalidation.ts new file mode 100644 index 00000000..309093c8 --- /dev/null +++ b/apps/app/src/hooks/useRevalidation.ts @@ -0,0 +1,8 @@ +import { useMutation } from "@tanstack/react-query"; +import { revalidate } from "../services/revalidation.service"; + +export const useRevalidation = () => { + const revalidateMutation = useMutation(revalidate); + + return { revalidateMutation }; +}; 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..7d7b237a --- /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.NEXT_PUBLIC_REVALIDATION_SECRET) { + 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" }); + } +} diff --git a/apps/app/src/services/revalidation.service.ts b/apps/app/src/services/revalidation.service.ts new file mode 100644 index 00000000..b5a601d7 --- /dev/null +++ b/apps/app/src/services/revalidation.service.ts @@ -0,0 +1,5 @@ +export const revalidate = (path: string) => + fetch( + `/api/revalidation?token=${process.env.NEXT_PUBLIC_REVALIDATION_SECRET || ""}&path=${path}`, + { method: "POST" }, + ); From 05475731d734e5fa4a4e1906d8fd83d6cdd4f6dc Mon Sep 17 00:00:00 2001 From: adipol1359 Date: Sun, 8 Jan 2023 19:14:07 +0100 Subject: [PATCH 2/5] refactor: move revalidation to the server --- apps/api/.env-example | 3 +++ apps/api/config/config.ts | 6 ++++++ apps/api/modules/answers/answers.routes.ts | 12 +++++++++++- apps/api/services/revalidation.service.ts | 12 ++++++++++++ apps/app/.env.local-example | 2 +- .../AddAnswerForm/AddAnswerForm.tsx | 16 ++++------------ apps/app/src/hooks/useRevalidation.ts | 8 -------- apps/app/src/pages/api/revalidation.ts | 2 +- apps/app/src/services/revalidation.service.ts | 5 ----- 9 files changed, 38 insertions(+), 28 deletions(-) create mode 100644 apps/api/services/revalidation.service.ts delete mode 100644 apps/app/src/hooks/useRevalidation.ts delete mode 100644 apps/app/src/services/revalidation.service.ts 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..89c74967 --- /dev/null +++ b/apps/api/services/revalidation.service.ts @@ -0,0 +1,12 @@ +import { getConfig } from "../config/config.js"; + +export const revalidate = (path: string) => { + console.log(getConfig("APP_URL")); + + return fetch( + `${getConfig("APP_URL")}/api/revalidation?token=${getConfig( + "REVALIDATION_TOKEN", + )}&path=${path}`, + { method: "POST" }, + ); +}; diff --git a/apps/app/.env.local-example b/apps/app/.env.local-example index f1c16b8e..f8e91e76 100644 --- a/apps/app/.env.local-example +++ b/apps/app/.env.local-example @@ -1,5 +1,5 @@ NEXT_PUBLIC_API_URL=http://api.devfaq.localhost:3002 NEXT_PUBLIC_APP_URL=http://app.devfaq.localhost:3000 -NEXT_PUBLIC_REVALIDATION_SECRET= +REVALIDATION_TOKEN= FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY= diff --git a/apps/app/src/components/QuestionAnswers/AddAnswerForm/AddAnswerForm.tsx b/apps/app/src/components/QuestionAnswers/AddAnswerForm/AddAnswerForm.tsx index 31bf3465..afe58440 100644 --- a/apps/app/src/components/QuestionAnswers/AddAnswerForm/AddAnswerForm.tsx +++ b/apps/app/src/components/QuestionAnswers/AddAnswerForm/AddAnswerForm.tsx @@ -7,7 +7,6 @@ import { Button } from "../../Button/Button"; import { WysiwygEditor } from "../../WysiwygEditor/WysiwygEditor"; import { URL_REGEX } from "../../../lib/constants"; import { Error } from "../../Error"; -import { useRevalidation } from "../../../hooks/useRevalidation"; import { AnswerSources } from "./AnswerSources"; type AddAnswerFormProps = Readonly<{ @@ -22,13 +21,11 @@ export const AddAnswerForm = ({ questionId }: AddAnswerFormProps) => { const [isError, setIsError] = useState(false); const { createQuestionAnswerMutation } = useQuestionMutation(); - const { revalidateMutation } = useRevalidation(); const disabled = content.trim().length === 0 || !sources.every((source) => URL_REGEX.test(source)) || - createQuestionAnswerMutation.isLoading || - revalidateMutation.isLoading; + createQuestionAnswerMutation.isLoading; const handleFormSubmit = (event: FormEvent) => { event.preventDefault(); @@ -41,14 +38,9 @@ export const AddAnswerForm = ({ questionId }: AddAnswerFormProps) => { }, { onSuccess: () => { - revalidateMutation.mutate(`/questions/p/${questionId}`, { - onSuccess: () => { - router.refresh(); - setContent(""); - setSources([]); - }, - onError: () => setIsError(true), - }); + router.refresh(); + setContent(""); + setSources([]); }, onError: () => setIsError(true), }, diff --git a/apps/app/src/hooks/useRevalidation.ts b/apps/app/src/hooks/useRevalidation.ts deleted file mode 100644 index 309093c8..00000000 --- a/apps/app/src/hooks/useRevalidation.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { revalidate } from "../services/revalidation.service"; - -export const useRevalidation = () => { - const revalidateMutation = useMutation(revalidate); - - return { revalidateMutation }; -}; diff --git a/apps/app/src/pages/api/revalidation.ts b/apps/app/src/pages/api/revalidation.ts index 7d7b237a..45a84ce7 100644 --- a/apps/app/src/pages/api/revalidation.ts +++ b/apps/app/src/pages/api/revalidation.ts @@ -4,7 +4,7 @@ import { validatePath } from "../../lib/revalidation"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { token, path } = req.query; - if (token !== process.env.NEXT_PUBLIC_REVALIDATION_SECRET) { + if (token !== process.env.REVALIDATION_TOKEN) { return res.status(401).json({ message: "Invalid token" }); } diff --git a/apps/app/src/services/revalidation.service.ts b/apps/app/src/services/revalidation.service.ts deleted file mode 100644 index b5a601d7..00000000 --- a/apps/app/src/services/revalidation.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const revalidate = (path: string) => - fetch( - `/api/revalidation?token=${process.env.NEXT_PUBLIC_REVALIDATION_SECRET || ""}&path=${path}`, - { method: "POST" }, - ); From f8683c6e6e1f97e848d4084a38f8f0bb4ecda219 Mon Sep 17 00:00:00 2001 From: adipol1359 Date: Sun, 8 Jan 2023 19:15:50 +0100 Subject: [PATCH 3/5] refactor(api): remove test code --- apps/api/services/revalidation.service.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/api/services/revalidation.service.ts b/apps/api/services/revalidation.service.ts index 89c74967..e47a009d 100644 --- a/apps/api/services/revalidation.service.ts +++ b/apps/api/services/revalidation.service.ts @@ -1,12 +1,9 @@ import { getConfig } from "../config/config.js"; -export const revalidate = (path: string) => { - console.log(getConfig("APP_URL")); - - return fetch( +export const revalidate = (path: string) => + fetch( `${getConfig("APP_URL")}/api/revalidation?token=${getConfig( "REVALIDATION_TOKEN", )}&path=${path}`, { method: "POST" }, ); -}; From f1d3587f796fce046ad72939a6e2ca0033d7d609 Mon Sep 17 00:00:00 2001 From: adipol1359 Date: Sun, 8 Jan 2023 19:33:30 +0100 Subject: [PATCH 4/5] feat(api): add fetch types --- apps/api/typings/undici/index.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 apps/api/typings/undici/index.d.ts 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 {}; From c17d1da3b82800b0d8fed6e4fbdcbd9b9fe9d7ff Mon Sep 17 00:00:00 2001 From: adipol1359 Date: Wed, 11 Jan 2023 18:44:15 +0100 Subject: [PATCH 5/5] fix(app): fix refetch after delete answer --- apps/app/src/components/QuestionAnswers/EditAnswer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) {