diff --git a/packages/payload-plugin-comments/README.md b/packages/payload-plugin-comments/README.md index a0aec81fe..47f464e7c 100644 --- a/packages/payload-plugin-comments/README.md +++ b/packages/payload-plugin-comments/README.md @@ -485,13 +485,13 @@ RESEND_FROM_EMAIL=comments@yourdomain.com 2. It creates a `comments` collection (hidden from the admin sidebar by default). 3. It patches **every collection** to inject `FieldCommentLabel` into each field's admin label and registers an `afterDelete` hook that cascade-deletes comments when a document is removed. 4. It patches **every global** to inject `FieldCommentLabel` into each field's admin label. -5. It registers two admin providers (`CommentsProviderWrapper`, `GlobalCommentsLoader`) and one admin action (`CommentsHeaderButton`). +5. It registers an admin provider (`CommentsProviderWrapper`) and one admin action (`CommentsHeaderButton`). +6. It registers REST endpoints under `/api/comments-plugin/*` (list, create, delete, resolve, mark-read, unread-mentions-count, mentionable-users, document-titles, field-labels). Each one requires an authenticated user and uses the Payload instance from the request, so it works on a freshly started serverless instance. -**Data loading** (`GlobalCommentsLoader`): +**Data loading** (`src/api`): -- This server component runs on every admin page load. -- It fetches all comments, document titles, mentionable users, field labels, collection labels, and global labels in parallel. -- Results are passed to `GlobalCommentsHydrator` (a client component) which hydrates the `CommentsContext`. +- The admin UI fetches comments, document titles, mentionable users, field labels, and unread mention counts with TanStack Query from the plugin's endpoints. +- Mutations (add, delete, resolve, mark as read) call the same endpoints and update the query cache optimistically. **State management** (`CommentsProvider`): @@ -526,10 +526,10 @@ RESEND_FROM_EMAIL=comments@yourdomain.com ## Exports Reference -| Import path | Exports | -| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -| `@focus-reactive/payload-plugin-comments` | `commentsPlugin`, `CommentsPluginConfig` (type), `CommentsTranslations` (type), `setPayloadConfig` | -| `@focus-reactive/payload-plugin-comments/styles.css` | Plugin stylesheet (Tailwind-compiled CSS) | +| Import path | Exports | +| ---------------------------------------------------- | ------------------------------------------------------------------------------- | +| `@focus-reactive/payload-plugin-comments` | `commentsPlugin`, `CommentsPluginConfig` (type), `CommentsTranslations` (type) | +| `@focus-reactive/payload-plugin-comments/styles.css` | Plugin stylesheet (Tailwind-compiled CSS) | --- diff --git a/packages/payload-plugin-comments/src/api/mutations/useAddCommentMutation.ts b/packages/payload-plugin-comments/src/api/mutations/useAddCommentMutation.ts index a27c5eb2f..2b10acc60 100644 --- a/packages/payload-plugin-comments/src/api/mutations/useAddCommentMutation.ts +++ b/packages/payload-plugin-comments/src/api/mutations/useAddCommentMutation.ts @@ -2,7 +2,9 @@ import { useMutation } from "@tanstack/react-query"; import { getCommentsKey, getFieldLabelsKey, getDocumentTitlesKey, QUERY_KEYS } from "../queryKeys"; -import { createComment } from "../../services/createComment"; +import type { CreateCommentArgs } from "../../services/createComment"; +import { useCommentsRequest } from "../useCommentsRequest"; +import { COMMENTS_ENDPOINT_PATHS } from "../../constants"; import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider"; import type { Comment, User, QueryContext } from "../../types"; @@ -20,12 +22,13 @@ interface AddCommentVariables { export function useAddCommentMutation() { const queryClient = useCommentsQueryClient(); + const request = useCommentsRequest(); return useMutation( { // eslint-disable-next-line @typescript-eslint/no-unused-vars mutationFn: ({ ctx: _ctx, currentUser: _user, ...data }: AddCommentVariables) => - createComment(data), + request(COMMENTS_ENDPOINT_PATHS.create, data), onMutate: async (variables) => { const { ctx, diff --git a/packages/payload-plugin-comments/src/api/mutations/useDeleteCommentMutation.ts b/packages/payload-plugin-comments/src/api/mutations/useDeleteCommentMutation.ts index b2ae4ad93..e9bbd2961 100644 --- a/packages/payload-plugin-comments/src/api/mutations/useDeleteCommentMutation.ts +++ b/packages/payload-plugin-comments/src/api/mutations/useDeleteCommentMutation.ts @@ -2,7 +2,9 @@ import { useMutation } from "@tanstack/react-query"; import { getCommentsKey } from "../queryKeys"; -import { deleteComment } from "../../services/deleteComment"; +import type { DeleteCommentArgs } from "../../services/deleteComment"; +import { useCommentsRequest } from "../useCommentsRequest"; +import { COMMENTS_ENDPOINT_PATHS } from "../../constants"; import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider"; import type { Comment, QueryContext } from "../../types"; @@ -13,10 +15,12 @@ interface DeleteCommentVariables { export function useDeleteCommentMutation() { const queryClient = useCommentsQueryClient(); + const request = useCommentsRequest(); return useMutation( { - mutationFn: ({ commentId }: DeleteCommentVariables) => deleteComment(commentId), + mutationFn: ({ commentId }: DeleteCommentVariables) => + request(COMMENTS_ENDPOINT_PATHS.delete, { id: commentId }), onMutate: async (variables) => { const { ctx, commentId } = variables; const key = getCommentsKey(ctx); diff --git a/packages/payload-plugin-comments/src/api/mutations/useMarkCommentReadMutation.ts b/packages/payload-plugin-comments/src/api/mutations/useMarkCommentReadMutation.ts index 5b90d1f1f..7443fd53a 100644 --- a/packages/payload-plugin-comments/src/api/mutations/useMarkCommentReadMutation.ts +++ b/packages/payload-plugin-comments/src/api/mutations/useMarkCommentReadMutation.ts @@ -1,7 +1,9 @@ "use client"; import { useMutation } from "@tanstack/react-query"; -import { markCommentRead } from "../../services/markCommentRead"; +import type { MarkCommentReadArgs } from "../../services/markCommentRead"; +import { useCommentsRequest } from "../useCommentsRequest"; +import { COMMENTS_ENDPOINT_PATHS } from "../../constants"; import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider"; import { useUnreadMentions } from "../../providers/UnreadMentionsProvider"; @@ -17,11 +19,15 @@ const UNREAD_COUNT_KEY_PREFIX = ["unread-mentions-count"] as const; export function useMarkCommentReadMutation() { const queryClient = useCommentsQueryClient(); + const request = useCommentsRequest(); const { rememberRead } = useUnreadMentions(); return useMutation( { - mutationFn: ({ commentId }: Variables) => markCommentRead({ commentId }), + mutationFn: ({ commentId }: Variables) => + request(COMMENTS_ENDPOINT_PATHS.markRead, { + commentId, + }), onMutate: async ({ commentId }) => { rememberRead(commentId); diff --git a/packages/payload-plugin-comments/src/api/mutations/useResolveCommentMutation.ts b/packages/payload-plugin-comments/src/api/mutations/useResolveCommentMutation.ts index f940b2a29..a0f454296 100644 --- a/packages/payload-plugin-comments/src/api/mutations/useResolveCommentMutation.ts +++ b/packages/payload-plugin-comments/src/api/mutations/useResolveCommentMutation.ts @@ -2,7 +2,9 @@ import { useMutation } from "@tanstack/react-query"; import { getCommentsKey } from "../queryKeys"; -import { resolveComment } from "../../services/resolveComment"; +import type { ResolveCommentArgs } from "../../services/resolveComment"; +import { useCommentsRequest } from "../useCommentsRequest"; +import { COMMENTS_ENDPOINT_PATHS } from "../../constants"; import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider"; import type { Comment, QueryContext } from "../../types"; @@ -15,11 +17,15 @@ interface ResolveCommentVariables { export function useResolveCommentMutation() { const queryClient = useCommentsQueryClient(); + const request = useCommentsRequest(); return useMutation( { mutationFn: ({ commentId, resolved }: ResolveCommentVariables) => - resolveComment(commentId, resolved), + request(COMMENTS_ENDPOINT_PATHS.resolve, { + id: commentId, + resolved, + }), onMutate: async (variables) => { const { ctx, commentId, resolved, currentUser } = variables; const key = getCommentsKey(ctx); diff --git a/packages/payload-plugin-comments/src/api/queries/useCommentsQuery.ts b/packages/payload-plugin-comments/src/api/queries/useCommentsQuery.ts index d9a1c6523..b65d346e1 100644 --- a/packages/payload-plugin-comments/src/api/queries/useCommentsQuery.ts +++ b/packages/payload-plugin-comments/src/api/queries/useCommentsQuery.ts @@ -3,17 +3,19 @@ import { useQuery } from "@tanstack/react-query"; import { useConfig } from "@payloadcms/ui"; import { getCommentsKey } from "../queryKeys"; -import { findAllComments } from "../../services/findAllComments"; +import type { FindAllCommentsArgs } from "../../services/findAllComments"; +import { useCommentsRequest } from "../useCommentsRequest"; import { useCommentsDrawer } from "../../providers/CommentsDrawerProvider"; import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider"; -import type { QueryContext } from "../../types"; +import type { Comment, QueryContext } from "../../types"; import type { CommentsPluginConfigStorage } from "../../types"; -import { REFETCH_INTERVAL } from "../../constants"; +import { COMMENTS_ENDPOINT_PATHS, REFETCH_INTERVAL } from "../../constants"; export function useCommentsQuery(ctx: QueryContext) { const queryClient = useCommentsQueryClient(); const { isOpen } = useCommentsDrawer(); const { config } = useConfig(); + const request = useCommentsRequest(); const pluginConfig = config.admin?.custom?.commentsPlugin as | CommentsPluginConfigStorage | undefined; @@ -22,7 +24,7 @@ export function useCommentsQuery(ctx: QueryContext) { { queryKey: getCommentsKey(ctx), queryFn: async () => { - const params = + const params: FindAllCommentsArgs = ctx.mode === "doc" ? { enabledCollections: pluginConfig?.collections, @@ -41,7 +43,10 @@ export function useCommentsQuery(ctx: QueryContext) { enabledGlobals: pluginConfig?.globals, }; - const res = await findAllComments(params); + const res = await request( + COMMENTS_ENDPOINT_PATHS.list, + params + ); if (!res.success) throw new Error(res.error); diff --git a/packages/payload-plugin-comments/src/api/queries/useDocumentTitlesQuery.ts b/packages/payload-plugin-comments/src/api/queries/useDocumentTitlesQuery.ts index 00c346d68..1b3407495 100644 --- a/packages/payload-plugin-comments/src/api/queries/useDocumentTitlesQuery.ts +++ b/packages/payload-plugin-comments/src/api/queries/useDocumentTitlesQuery.ts @@ -4,13 +4,14 @@ import { useQuery } from "@tanstack/react-query"; import { useConfig } from "@payloadcms/ui"; import { useLocale } from "@payloadcms/ui"; import { getDocumentTitlesKey } from "../queryKeys"; -import { getDocumentTitles } from "../../services/getDocumentTitles"; +import type { GetDocumentTitlesArgs } from "../../services/getDocumentTitles"; +import { useCommentsRequest } from "../useCommentsRequest"; import { useCommentsDrawer } from "../../providers/CommentsDrawerProvider"; import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider"; import { useCommentsQuery } from "./useCommentsQuery"; -import type { QueryContext } from "../../types"; +import type { DocumentTitles, QueryContext } from "../../types"; import type { CommentsPluginConfigStorage } from "../../types"; -import { REFETCH_INTERVAL } from "../../constants"; +import { COMMENTS_ENDPOINT_PATHS, REFETCH_INTERVAL } from "../../constants"; export function useDocumentTitlesQuery(ctx: QueryContext) { const queryClient = useCommentsQueryClient(); @@ -18,6 +19,7 @@ export function useDocumentTitlesQuery(ctx: QueryContext) { const { data: comments } = useCommentsQuery(ctx); const { code: locale } = useLocale(); const { config } = useConfig(); + const request = useCommentsRequest(); const pluginConfig = config.admin?.custom?.commentsPlugin as | CommentsPluginConfigStorage @@ -27,10 +29,13 @@ export function useDocumentTitlesQuery(ctx: QueryContext) { { queryKey: getDocumentTitlesKey(ctx), queryFn: async () => { - const res = await getDocumentTitles( - comments ?? [], - pluginConfig?.documentTitleFields ?? {}, - { locale } + const res = await request( + COMMENTS_ENDPOINT_PATHS.documentTitles, + { + comments: comments ?? [], + documentTitleFields: pluginConfig?.documentTitleFields ?? {}, + locale, + } ); if (!res.success) throw new Error(res.error); diff --git a/packages/payload-plugin-comments/src/api/queries/useFieldLabelsQuery.ts b/packages/payload-plugin-comments/src/api/queries/useFieldLabelsQuery.ts index 1cccf37c4..e1de5dea8 100644 --- a/packages/payload-plugin-comments/src/api/queries/useFieldLabelsQuery.ts +++ b/packages/payload-plugin-comments/src/api/queries/useFieldLabelsQuery.ts @@ -2,22 +2,33 @@ import { useQuery } from "@tanstack/react-query"; import { getFieldLabelsKey } from "../queryKeys"; -import { fetchFieldLabels } from "../../services/fieldLabels/fetchFieldLabels"; +import type { FetchFieldLabelsArgs } from "../../services/fieldLabels/fetchFieldLabels"; +import { useCommentsRequest } from "../useCommentsRequest"; import { useCommentsDrawer } from "../../providers/CommentsDrawerProvider"; import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider"; import { useCommentsQuery } from "./useCommentsQuery"; -import type { QueryContext } from "../../types"; -import { REFETCH_INTERVAL } from "../../constants"; +import type { GlobalFieldLabelRegistry, QueryContext } from "../../types"; +import { COMMENTS_ENDPOINT_PATHS, REFETCH_INTERVAL } from "../../constants"; export function useFieldLabelsQuery(ctx: QueryContext) { const queryClient = useCommentsQueryClient(); const { isOpen } = useCommentsDrawer(); const { data: comments } = useCommentsQuery(ctx); + const request = useCommentsRequest(); return useQuery( { queryKey: getFieldLabelsKey(ctx), - queryFn: () => fetchFieldLabels(comments ?? []), + queryFn: async () => { + const res = await request( + COMMENTS_ENDPOINT_PATHS.fieldLabels, + { comments: comments ?? [] } + ); + + if (!res.success) throw new Error(res.error); + + return res.data; + }, enabled: isOpen && !!comments, staleTime: 0, refetchInterval: isOpen ? REFETCH_INTERVAL : false, diff --git a/packages/payload-plugin-comments/src/api/queries/useMentionableUsersQuery.ts b/packages/payload-plugin-comments/src/api/queries/useMentionableUsersQuery.ts index f4b49174d..42175a12c 100644 --- a/packages/payload-plugin-comments/src/api/queries/useMentionableUsersQuery.ts +++ b/packages/payload-plugin-comments/src/api/queries/useMentionableUsersQuery.ts @@ -2,20 +2,22 @@ import { useQuery } from "@tanstack/react-query"; import { QUERY_KEYS } from "../queryKeys"; -import { fetchMentionableUsers } from "../../services/fetchMentionableUsers"; +import { useCommentsRequest } from "../useCommentsRequest"; import { useCommentsDrawer } from "../../providers/CommentsDrawerProvider"; import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider"; -import { REFETCH_INTERVAL } from "../../constants"; +import { COMMENTS_ENDPOINT_PATHS, REFETCH_INTERVAL } from "../../constants"; +import type { User } from "../../types"; export function useMentionableUsersQuery() { const queryClient = useCommentsQueryClient(); const { isOpen } = useCommentsDrawer(); + const request = useCommentsRequest(); return useQuery( { queryKey: QUERY_KEYS.mentionableUsers(), queryFn: async () => { - const res = await fetchMentionableUsers(); + const res = await request(COMMENTS_ENDPOINT_PATHS.mentionableUsers, {}); if (!res.success) throw new Error(res.error); diff --git a/packages/payload-plugin-comments/src/api/queries/useUnreadMentionsCountQuery.ts b/packages/payload-plugin-comments/src/api/queries/useUnreadMentionsCountQuery.ts index 59f324400..f19afb821 100644 --- a/packages/payload-plugin-comments/src/api/queries/useUnreadMentionsCountQuery.ts +++ b/packages/payload-plugin-comments/src/api/queries/useUnreadMentionsCountQuery.ts @@ -2,9 +2,10 @@ import { useQuery } from "@tanstack/react-query"; import { QUERY_KEYS } from "../queryKeys"; -import { countUnreadMentions } from "../../services/countUnreadMentions"; +import type { CountUnreadMentionsArgs } from "../../services/countUnreadMentions"; +import { useCommentsRequest } from "../useCommentsRequest"; import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider"; -import { UNREAD_COUNT_REFETCH_INTERVAL } from "../../constants"; +import { COMMENTS_ENDPOINT_PATHS, UNREAD_COUNT_REFETCH_INTERVAL } from "../../constants"; import type { Mode } from "../../types"; interface Props { @@ -23,6 +24,7 @@ export function useUnreadMentionsCountQuery({ locale, }: Props) { const queryClient = useCommentsQueryClient(); + const request = useCommentsRequest(); return useQuery( { @@ -34,13 +36,16 @@ export function useUnreadMentionsCountQuery({ locale ), queryFn: async () => { - const res = await countUnreadMentions({ - mode, - collectionSlug, - documentId, - globalSlug, - locale, - }); + const res = await request( + COMMENTS_ENDPOINT_PATHS.unreadMentionsCount, + { + mode, + collectionSlug, + documentId, + globalSlug, + locale, + } + ); if (!res.success) throw new Error(res.error); diff --git a/packages/payload-plugin-comments/src/api/useCommentsRequest.ts b/packages/payload-plugin-comments/src/api/useCommentsRequest.ts new file mode 100644 index 000000000..26c365cb2 --- /dev/null +++ b/packages/payload-plugin-comments/src/api/useCommentsRequest.ts @@ -0,0 +1,40 @@ +"use client"; + +import { useCallback } from "react"; +import { useConfig } from "@payloadcms/ui"; +import { formatAdminURL } from "payload/shared"; +import type { Response } from "../types"; +import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage"; + +function isServiceResponse(value: unknown): value is Response { + return typeof value === "object" && value !== null && "success" in value; +} + +export function useCommentsRequest() { + const { + config: { + routes: { api: apiRoute }, + }, + } = useConfig(); + + return useCallback( + async (path: `/${string}`, args: TArgs): Promise> => { + try { + const res = await fetch(formatAdminURL({ apiRoute, path }), { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(args), + }); + const body: unknown = await res.json().catch(() => null); + + if (isServiceResponse(body)) return body; + + return { success: false, error: res.statusText || `Request failed with ${res.status}` }; + } catch (err) { + return { success: false, error: getDefaultErrorMessage(err) }; + } + }, + [apiRoute] + ); +} diff --git a/packages/payload-plugin-comments/src/components/CommentEditor/index.tsx b/packages/payload-plugin-comments/src/components/CommentEditor/index.tsx index fb530d0b4..e4aedfbee 100644 --- a/packages/payload-plugin-comments/src/components/CommentEditor/index.tsx +++ b/packages/payload-plugin-comments/src/components/CommentEditor/index.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useImperativeHandle, startTransition } from "react"; import type { RefObject } from "react"; -import { useAuth, useLocale, useTranslation } from "@payloadcms/ui"; +import { toast, useAuth, useLocale, useTranslation } from "@payloadcms/ui"; import { useComments } from "../../providers/CommentsProvider"; import { useMentionableUsersQuery } from "../../api/queries/useMentionableUsersQuery"; import { MentionDropdown } from "../MentionDropdown"; @@ -211,6 +211,13 @@ export function CommentEditor({ } }; + const restoreEditor = (html: string) => { + if (!editorRef.current) return; + + editorRef.current.innerHTML = html; + updateEmptyClass(); + }; + const focusEditor = () => { editorRef.current?.focus(); }; @@ -227,6 +234,8 @@ export function CommentEditor({ const serialized = getEditorValue(); if (!serialized) return; + const draft = editorRef.current?.innerHTML ?? ""; + clearEditor(); startTransition(async () => { @@ -241,7 +250,12 @@ export function CommentEditor({ if (res.success) { onSuccessAddComment?.(); + + return; } + + restoreEditor(draft); + toast.error(t("comments:failedToAdd" as never) ?? "Failed to add comment"); }); }; diff --git a/packages/payload-plugin-comments/src/config.ts b/packages/payload-plugin-comments/src/config.ts deleted file mode 100644 index f633a9bfc..000000000 --- a/packages/payload-plugin-comments/src/config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { SanitizedConfig } from "payload"; -import { PLUGIN_NAME } from "./constants"; - -const GLOBAL_CONFIG_KEY = `__${PLUGIN_NAME}_payload_config__`; - -type GlobalThis = Record; - -export function setPayloadConfig(config: SanitizedConfig) { - (globalThis as GlobalThis)[GLOBAL_CONFIG_KEY] = config; -} - -export function getPayloadConfig(): SanitizedConfig { - const config = (globalThis as GlobalThis)[GLOBAL_CONFIG_KEY] as SanitizedConfig | undefined; - - if (!config) { - throw new Error(`[${PLUGIN_NAME}] Payload config not initialized.`); - } - - return config; -} diff --git a/packages/payload-plugin-comments/src/constants.ts b/packages/payload-plugin-comments/src/constants.ts index 8d1c205f7..abaee1522 100644 --- a/packages/payload-plugin-comments/src/constants.ts +++ b/packages/payload-plugin-comments/src/constants.ts @@ -21,3 +21,15 @@ export const UNREAD_COUNT_REFETCH_INTERVAL = 30000; export const READ_DWELL_MS = 600; export const READ_OBSERVER_THRESHOLD = 0.5; export const UNREAD_BADGE_CAP = 9; + +export const COMMENTS_ENDPOINT_PATHS = { + list: "/comments-plugin/list", + create: "/comments-plugin/create", + delete: "/comments-plugin/delete", + resolve: "/comments-plugin/resolve", + markRead: "/comments-plugin/mark-read", + unreadMentionsCount: "/comments-plugin/unread-mentions-count", + mentionableUsers: "/comments-plugin/mentionable-users", + documentTitles: "/comments-plugin/document-titles", + fieldLabels: "/comments-plugin/field-labels", +} as const; diff --git a/packages/payload-plugin-comments/src/endpoints/defineEndpoint.ts b/packages/payload-plugin-comments/src/endpoints/defineEndpoint.ts new file mode 100644 index 000000000..b44e75812 --- /dev/null +++ b/packages/payload-plugin-comments/src/endpoints/defineEndpoint.ts @@ -0,0 +1,48 @@ +import type { Endpoint } from "payload"; +import { PLUGIN_NAME } from "../constants"; +import type { Response as ServiceResponse, ServiceContext } from "../types"; +import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage"; + +type EndpointService = ( + context: ServiceContext, + args: TArgs +) => Promise>; + +export function defineEndpoint( + path: string, + service: EndpointService +): Endpoint { + return { + path, + method: "post", + handler: async (req) => { + if (!req.user) { + return Response.json({ success: false, error: "Unauthorized" }, { status: 401 }); + } + + let args: TArgs; + + try { + args = (await req.json?.()) as TArgs; + } catch { + return Response.json({ success: false, error: "Invalid JSON body" }, { status: 400 }); + } + + try { + const result = await service( + { payload: req.payload, user: req.user, headers: req.headers }, + args + ); + + return Response.json(result, { status: result.success ? 200 : 400 }); + } catch (err) { + req.payload.logger.error({ err, msg: `[${PLUGIN_NAME}] ${path} failed` }); + + return Response.json( + { success: false, error: getDefaultErrorMessage(err) }, + { status: 500 } + ); + } + }, + }; +} diff --git a/packages/payload-plugin-comments/src/endpoints/index.ts b/packages/payload-plugin-comments/src/endpoints/index.ts new file mode 100644 index 000000000..0cded520c --- /dev/null +++ b/packages/payload-plugin-comments/src/endpoints/index.ts @@ -0,0 +1,24 @@ +import type { Endpoint } from "payload"; +import { COMMENTS_ENDPOINT_PATHS } from "../constants"; +import { countUnreadMentions } from "../services/countUnreadMentions"; +import { createComment } from "../services/createComment"; +import { deleteComment } from "../services/deleteComment"; +import { fetchMentionableUsers } from "../services/fetchMentionableUsers"; +import { fetchFieldLabels } from "../services/fieldLabels/fetchFieldLabels"; +import { findAllComments } from "../services/findAllComments"; +import { getDocumentTitles } from "../services/getDocumentTitles"; +import { markCommentRead } from "../services/markCommentRead"; +import { resolveComment } from "../services/resolveComment"; +import { defineEndpoint } from "./defineEndpoint"; + +export const commentsEndpoints: Endpoint[] = [ + defineEndpoint(COMMENTS_ENDPOINT_PATHS.list, findAllComments), + defineEndpoint(COMMENTS_ENDPOINT_PATHS.create, createComment), + defineEndpoint(COMMENTS_ENDPOINT_PATHS.delete, deleteComment), + defineEndpoint(COMMENTS_ENDPOINT_PATHS.resolve, resolveComment), + defineEndpoint(COMMENTS_ENDPOINT_PATHS.markRead, markCommentRead), + defineEndpoint(COMMENTS_ENDPOINT_PATHS.unreadMentionsCount, countUnreadMentions), + defineEndpoint(COMMENTS_ENDPOINT_PATHS.mentionableUsers, fetchMentionableUsers), + defineEndpoint(COMMENTS_ENDPOINT_PATHS.documentTitles, getDocumentTitles), + defineEndpoint(COMMENTS_ENDPOINT_PATHS.fieldLabels, fetchFieldLabels), +]; diff --git a/packages/payload-plugin-comments/src/index.ts b/packages/payload-plugin-comments/src/index.ts index 4fa40f5c6..c9280b3b7 100644 --- a/packages/payload-plugin-comments/src/index.ts +++ b/packages/payload-plugin-comments/src/index.ts @@ -1,4 +1,3 @@ export type { CommentsPluginConfig } from "./types"; export type { CommentsTranslations } from "./translations/types"; export { commentsPlugin } from "./plugin"; -export { setPayloadConfig } from "./config"; diff --git a/packages/payload-plugin-comments/src/plugin.ts b/packages/payload-plugin-comments/src/plugin.ts index d2ecac44b..f5b782884 100644 --- a/packages/payload-plugin-comments/src/plugin.ts +++ b/packages/payload-plugin-comments/src/plugin.ts @@ -8,7 +8,7 @@ import { normalizeCollections } from "./utils/config/normalizeCollections"; import { mergeTranslations } from "./utils/config/mergeTranslations"; import { overrideCollections } from "./utils/config/overrideCollections"; import { overrideGlobals } from "./utils/config/overrideGlobals"; -import { setPayloadConfig } from "./config"; +import { commentsEndpoints } from "./endpoints"; export const commentsPlugin = (config: CommentsPluginConfig = {}): Plugin => @@ -67,11 +67,7 @@ export const commentsPlugin = }, }, }, - onInit: async (payload) => { - setPayloadConfig(payload.config); - - await incomingConfig.onInit?.(payload); - }, + endpoints: [...(incomingConfig.endpoints ?? []), ...commentsEndpoints], collections: [ ...overrideCollections(incomingConfig.collections), finalCollection, diff --git a/packages/payload-plugin-comments/src/services/countUnreadMentions.ts b/packages/payload-plugin-comments/src/services/countUnreadMentions.ts index a99194455..d1f21bd13 100644 --- a/packages/payload-plugin-comments/src/services/countUnreadMentions.ts +++ b/packages/payload-plugin-comments/src/services/countUnreadMentions.ts @@ -1,14 +1,10 @@ -"use server"; - -import { headers } from "next/headers"; import type { Where } from "payload"; import { COMMENT_READS_COLLECTION_SLUG, DEFAULT_COLLECTION_SLUG } from "../constants"; -import { extractPayload } from "../utils/payload/extractPayload"; import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage"; import { getCurrentTenantId } from "./getCurrentTenantId"; -import type { BaseServiceOptions, CommentsPluginConfigStorage, Mode, Response } from "../types"; +import type { CommentsPluginConfigStorage, Mode, Response, ServiceContext } from "../types"; -interface Props extends BaseServiceOptions { +export interface CountUnreadMentionsArgs { enabledCollections?: string[]; enabledGlobals?: string[]; mode: Mode; @@ -22,29 +18,26 @@ interface WhereWithRequiredAnd extends Where { and: Where[]; } -export async function countUnreadMentions({ - enabledCollections, - enabledGlobals, - payload: payloadProp, - mode, - collectionSlug, - documentId, - globalSlug, - locale, -}: Props): Promise> { +export async function countUnreadMentions( + { payload, user, headers }: ServiceContext, + { + enabledCollections, + enabledGlobals, + mode, + collectionSlug, + documentId, + globalSlug, + locale, + }: CountUnreadMentionsArgs +): Promise> { try { - const payload = await extractPayload(payloadProp); - const { user } = await payload.auth({ headers: await headers() }); - - if (!user) return { success: true, data: { count: 0 } }; - const pluginConfig = payload.config.admin?.custom?.commentsPlugin as | CommentsPluginConfigStorage | undefined; const collections = enabledCollections ?? pluginConfig?.collections ?? []; const globals = enabledGlobals ?? pluginConfig?.globals ?? []; - const tenantId = await getCurrentTenantId(payload); + const tenantId = getCurrentTenantId(payload, headers); const where: WhereWithRequiredAnd = { and: [ diff --git a/packages/payload-plugin-comments/src/services/createComment.ts b/packages/payload-plugin-comments/src/services/createComment.ts index 11b2e586f..c901465eb 100644 --- a/packages/payload-plugin-comments/src/services/createComment.ts +++ b/packages/payload-plugin-comments/src/services/createComment.ts @@ -1,42 +1,31 @@ -"use server"; - -import { headers } from "next/headers"; import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage"; -import type { Response, Comment, BaseServiceOptions } from "../types"; +import type { Response, Comment, ServiceContext } from "../types"; import { DEFAULT_COLLECTION_SLUG, FALLBACK_USERNAME } from "../constants"; import { sendMentionEmails } from "./sendMentionEmails"; -import { extractPayload } from "../utils/payload/extractPayload"; -interface Props extends BaseServiceOptions { +export interface CreateCommentArgs { documentId?: number | null; collectionSlug?: string | null; globalSlug?: string | null; text: string; fieldPath?: string | null; mentionIds?: number[]; + locale?: string | null; } -export async function createComment({ - documentId, - collectionSlug, - globalSlug, - text, - fieldPath = null, - mentionIds = [], - locale = null, - payload: payloadProp, -}: Props): Promise> { +export async function createComment( + { payload, user }: ServiceContext, + { + documentId, + collectionSlug, + globalSlug, + text, + fieldPath = null, + mentionIds = [], + locale = null, + }: CreateCommentArgs +): Promise> { try { - const payload = await extractPayload(payloadProp); - const { user } = await payload.auth({ headers: await headers() }); - - if (!user) { - return { - success: false, - error: "Unauthorized", - }; - } - if (!globalSlug && (!documentId || !collectionSlug)) { return { success: false, diff --git a/packages/payload-plugin-comments/src/services/deleteComment.ts b/packages/payload-plugin-comments/src/services/deleteComment.ts index 307af7d10..2d0602138 100644 --- a/packages/payload-plugin-comments/src/services/deleteComment.ts +++ b/packages/payload-plugin-comments/src/services/deleteComment.ts @@ -1,23 +1,16 @@ -"use server"; - -import { headers } from "next/headers"; import { DEFAULT_COLLECTION_SLUG } from "../constants"; import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage"; -import type { BaseServiceOptions, Response, Comment } from "../types"; -import { extractPayload } from "../utils/payload/extractPayload"; +import type { Response, Comment, ServiceContext } from "../types"; + +export interface DeleteCommentArgs { + id: number | string; +} export async function deleteComment( - id: number | string, - options?: BaseServiceOptions + { payload, user }: ServiceContext, + { id }: DeleteCommentArgs ): Promise> { try { - const payload = await extractPayload(options?.payload); - const { user } = await payload.auth({ headers: await headers() }); - - if (!user) { - return { success: false, error: "Unauthorized" }; - } - const data = (await payload.delete({ collection: DEFAULT_COLLECTION_SLUG, id, diff --git a/packages/payload-plugin-comments/src/services/fetchMentionableUsers.ts b/packages/payload-plugin-comments/src/services/fetchMentionableUsers.ts index 3ff73e120..c01505f31 100644 --- a/packages/payload-plugin-comments/src/services/fetchMentionableUsers.ts +++ b/packages/payload-plugin-comments/src/services/fetchMentionableUsers.ts @@ -1,25 +1,21 @@ -"use server"; - import type { Where } from "payload"; -import type { BaseServiceOptions, CommentsPluginConfigStorage, Response, User } from "../types"; +import type { CommentsPluginConfigStorage, Response, ServiceContext, User } from "../types"; import { resolveUsername } from "../utils/user/resolveUsername"; import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage"; -import { extractPayload } from "../utils/payload/extractPayload"; import { getCurrentTenantId } from "./getCurrentTenantId"; import { FALLBACK_USERNAME, USERNAME_DEFAULT_FIELD_PATH } from "../constants"; -export async function fetchMentionableUsers( - options?: BaseServiceOptions -): Promise> { +export async function fetchMentionableUsers({ + payload, + headers, +}: ServiceContext): Promise> { try { - const payload = await extractPayload(options?.payload); - const pluginConfig = payload.config.admin?.custom?.commentsPlugin as | CommentsPluginConfigStorage | undefined; const usernameFieldPath = pluginConfig?.usernameFieldPath ?? USERNAME_DEFAULT_FIELD_PATH; - const tenantId = await getCurrentTenantId(payload); + const tenantId = getCurrentTenantId(payload, headers); const where: Where = tenantId ? { diff --git a/packages/payload-plugin-comments/src/services/fieldLabels/fetchFieldLabels.ts b/packages/payload-plugin-comments/src/services/fieldLabels/fetchFieldLabels.ts index a896ae936..ab5b6bbb9 100644 --- a/packages/payload-plugin-comments/src/services/fieldLabels/fetchFieldLabels.ts +++ b/packages/payload-plugin-comments/src/services/fieldLabels/fetchFieldLabels.ts @@ -1,8 +1,5 @@ -"use server"; - import type { CollectionSlug, Field } from "payload"; -import type { BaseServiceOptions, Comment, GlobalFieldLabelRegistry } from "../../types"; -import { extractPayload } from "../../utils/payload/extractPayload"; +import type { Comment, GlobalFieldLabelRegistry, Response, ServiceContext } from "../../types"; import { groupFieldPathsByDocument } from "./utils/groupFieldPathsByDocument"; import { needsDocumentFetch } from "./utils/schemaUtils"; import { resolveFieldPath } from "./utils/resolveFieldPath"; @@ -20,14 +17,16 @@ type BaseGlobals = Record; type BaseDocumentData = Record; +export interface FetchFieldLabelsArgs { + comments: Comment[]; +} + export async function fetchFieldLabels( - comments: Comment[], - options?: BaseServiceOptions -): Promise { + { payload }: ServiceContext, + { comments }: FetchFieldLabelsArgs +): Promise> { const registry: GlobalFieldLabelRegistry = {}; - const payload = await extractPayload(options?.payload); - // --- Collections branch --- const fieldPathsMap = groupFieldPathsByDocument(comments); const collections = payload.collections as BasePayloadCollections; @@ -110,5 +109,5 @@ export async function fetchFieldLabels( } } - return registry; + return { success: true, data: registry }; } diff --git a/packages/payload-plugin-comments/src/services/findAllComments.ts b/packages/payload-plugin-comments/src/services/findAllComments.ts index 14fc05797..2bc055f82 100644 --- a/packages/payload-plugin-comments/src/services/findAllComments.ts +++ b/packages/payload-plugin-comments/src/services/findAllComments.ts @@ -1,34 +1,29 @@ -"use server"; - -import { headers } from "next/headers"; -import type { TypedUser, Where } from "payload"; -import { extractPayload } from "../utils/payload/extractPayload"; +import type { Where } from "payload"; import { COMMENT_READS_COLLECTION_SLUG, DEFAULT_COLLECTION_SLUG } from "../constants"; import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage"; -import type { Response, Comment, BaseServiceOptions } from "../types"; +import type { Response, Comment, ServiceContext } from "../types"; import { getCurrentTenantId } from "./getCurrentTenantId"; -interface Props { +export interface FindAllCommentsArgs { enabledCollections?: string[]; enabledGlobals?: string[]; - user?: TypedUser | null; - options?: BaseServiceOptions; docId?: string | number; filterCollectionSlug?: string; filterGlobalSlug?: string; } -export async function findAllComments({ - enabledCollections, - enabledGlobals, - options, - docId, - filterCollectionSlug, - filterGlobalSlug, -}: Props = {}): Promise> { +export async function findAllComments( + { payload, user, headers }: ServiceContext, + { + enabledCollections, + enabledGlobals, + docId, + filterCollectionSlug, + filterGlobalSlug, + }: FindAllCommentsArgs +): Promise> { try { - const payload = await extractPayload(options?.payload); - const tenantId = await getCurrentTenantId(payload); + const tenantId = getCurrentTenantId(payload, headers); const where: Where = {}; @@ -80,11 +75,9 @@ export async function findAllComments({ overrideAccess: true, }); - const { user } = await payload.auth({ headers: await headers() }); - let readSet: Set | null = null; - if (user && comments.length > 0) { + if (comments.length > 0) { const commentIds = comments.map((c) => c.id as number); const { docs: reads } = await payload.find({ diff --git a/packages/payload-plugin-comments/src/services/getCurrentTenantId.ts b/packages/payload-plugin-comments/src/services/getCurrentTenantId.ts index 32f3c3865..d034ddb54 100644 --- a/packages/payload-plugin-comments/src/services/getCurrentTenantId.ts +++ b/packages/payload-plugin-comments/src/services/getCurrentTenantId.ts @@ -1,11 +1,8 @@ -"use server"; - import type { Payload } from "payload"; -import { headers as getHeaders } from "next/headers"; import { getTenantFromCookie } from "@payloadcms/plugin-multi-tenant/utilities"; import type { CommentsPluginConfigStorage } from "../types"; -export async function getCurrentTenantId(payload: Payload) { +export function getCurrentTenantId(payload: Payload, headers: Headers) { const pluginConfig = payload.config.admin?.custom?.commentsPlugin as | CommentsPluginConfigStorage | undefined; @@ -14,5 +11,5 @@ export async function getCurrentTenantId(payload: Payload) { if (!tenantConfig?.enabled) return null; - return getTenantFromCookie(await getHeaders(), payload.db.defaultIDType); + return getTenantFromCookie(headers, payload.db.defaultIDType); } diff --git a/packages/payload-plugin-comments/src/services/getDocumentTitles.ts b/packages/payload-plugin-comments/src/services/getDocumentTitles.ts index b5cf3aff9..1e55d1468 100644 --- a/packages/payload-plugin-comments/src/services/getDocumentTitles.ts +++ b/packages/payload-plugin-comments/src/services/getDocumentTitles.ts @@ -1,9 +1,12 @@ -"use server"; - import type { CollectionSlug } from "payload"; -import { extractPayload } from "../utils/payload/extractPayload"; import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage"; -import type { Response, Comment, BaseDocument, DocumentTitles, BaseServiceOptions } from "../types"; +import type { Response, Comment, BaseDocument, DocumentTitles, ServiceContext } from "../types"; + +export interface GetDocumentTitlesArgs { + comments: Comment[]; + documentTitleFields: Record; + locale?: string | null; +} function buildDocumentTitlesFromDocs( docs: BaseDocument[], @@ -21,13 +24,10 @@ function buildDocumentTitlesFromDocs( } export async function getDocumentTitles( - comments: Comment[], - documentTitleFields: Record, - options?: BaseServiceOptions + { payload }: ServiceContext, + { comments, documentTitleFields, locale }: GetDocumentTitlesArgs ): Promise> { try { - const payload = await extractPayload(options?.payload); - const documentIdsMap = new Map>(); for (const { collectionSlug, documentId } of comments) { @@ -57,7 +57,7 @@ export async function getDocumentTitles( limit: ids.size, depth: 0, overrideAccess: true, - locale: options?.locale, + locale, }); documentTitles[slug] = buildDocumentTitlesFromDocs( diff --git a/packages/payload-plugin-comments/src/services/markCommentRead.ts b/packages/payload-plugin-comments/src/services/markCommentRead.ts index c46c56ee6..fd5d4263d 100644 --- a/packages/payload-plugin-comments/src/services/markCommentRead.ts +++ b/packages/payload-plugin-comments/src/services/markCommentRead.ts @@ -1,13 +1,9 @@ -"use server"; - -import { headers } from "next/headers"; import { COMMENT_READS_COLLECTION_SLUG, DEFAULT_COLLECTION_SLUG } from "../constants"; -import { extractPayload } from "../utils/payload/extractPayload"; import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage"; import { getCurrentTenantId } from "./getCurrentTenantId"; -import type { BaseServiceOptions, Comment, CommentMention, Response } from "../types"; +import type { Comment, CommentMention, Response, ServiceContext } from "../types"; -interface Props extends BaseServiceOptions { +export interface MarkCommentReadArgs { commentId: number; } @@ -17,16 +13,11 @@ function extractRelationId(value: number | { id: number } | null | undefined): n return null; } -export async function markCommentRead({ - commentId, - payload: payloadProp, -}: Props): Promise> { +export async function markCommentRead( + { payload, user, headers }: ServiceContext, + { commentId }: MarkCommentReadArgs +): Promise> { try { - const payload = await extractPayload(payloadProp); - const { user } = await payload.auth({ headers: await headers() }); - - if (!user) return { success: false, error: "Unauthorized" }; - const comment = (await payload.findByID({ collection: DEFAULT_COLLECTION_SLUG, id: commentId, @@ -41,7 +32,7 @@ export async function markCommentRead({ }; } - const tenantId = await getCurrentTenantId(payload); + const tenantId = getCurrentTenantId(payload, headers); if ( tenantId && extractRelationId(comment.tenant as number | { id: number } | null) !== tenantId diff --git a/packages/payload-plugin-comments/src/services/resolveComment.ts b/packages/payload-plugin-comments/src/services/resolveComment.ts index 4b49323db..189a37431 100644 --- a/packages/payload-plugin-comments/src/services/resolveComment.ts +++ b/packages/payload-plugin-comments/src/services/resolveComment.ts @@ -1,27 +1,17 @@ -"use server"; - -import type { Response, Comment, BaseServiceOptions } from "../types"; -import { headers } from "next/headers"; +import type { Response, Comment, ServiceContext } from "../types"; import { DEFAULT_COLLECTION_SLUG } from "../constants"; import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage"; -import { extractPayload } from "../utils/payload/extractPayload"; + +export interface ResolveCommentArgs { + id: number | string; + resolved: boolean; +} export async function resolveComment( - id: number | string, - resolved: boolean, - options?: BaseServiceOptions + { payload, user }: ServiceContext, + { id, resolved }: ResolveCommentArgs ): Promise> { try { - const payload = await extractPayload(options?.payload); - const { user } = await payload.auth({ headers: await headers() }); - - if (!user) { - return { - success: false, - error: "Unauthorized", - }; - } - const res = await payload.update({ collection: DEFAULT_COLLECTION_SLUG, id, diff --git a/packages/payload-plugin-comments/src/services/sendMentionEmails.ts b/packages/payload-plugin-comments/src/services/sendMentionEmails.ts index 05ea4ff3e..dffe60295 100644 --- a/packages/payload-plugin-comments/src/services/sendMentionEmails.ts +++ b/packages/payload-plugin-comments/src/services/sendMentionEmails.ts @@ -1,12 +1,13 @@ import { Resend } from "resend"; import { getServerSideURL } from "../utils/general/getURL"; -import type { BaseServiceOptions, CommentsPluginConfigStorage, User } from "../types"; -import { extractPayload } from "../utils/payload/extractPayload"; +import type { Payload } from "payload"; +import type { CommentsPluginConfigStorage, User } from "../types"; import { FALLBACK_USERNAME, PLUGIN_NAME, USERNAME_DEFAULT_FIELD_PATH } from "../constants"; const PREFIX = `[${PLUGIN_NAME}]`; -interface SendMentionEmailsProps extends BaseServiceOptions { +interface SendMentionEmailsProps { + payload: Payload; mentionIds: number[]; authorName: string; commentText: string; @@ -20,10 +21,8 @@ export async function sendMentionEmails({ commentText, collectionSlug, documentId, - payload: payloadProp, + payload, }: SendMentionEmailsProps) { - const payload = await extractPayload(payloadProp); - const pluginConfig = payload.config.admin?.custom?.commentsPlugin as | CommentsPluginConfigStorage | undefined; diff --git a/packages/payload-plugin-comments/src/types/base.ts b/packages/payload-plugin-comments/src/types/base.ts index 1e25c1ce4..21a9baacf 100644 --- a/packages/payload-plugin-comments/src/types/base.ts +++ b/packages/payload-plugin-comments/src/types/base.ts @@ -1,8 +1,9 @@ -import type { Payload } from "payload"; +import type { Payload, TypedUser } from "payload"; -export interface BaseServiceOptions { - payload?: Payload; - locale?: string | null; +export interface ServiceContext { + payload: Payload; + user: TypedUser; + headers: Headers; } export interface BaseDocument { diff --git a/packages/payload-plugin-comments/src/types/index.ts b/packages/payload-plugin-comments/src/types/index.ts index 517bf0ee4..d7ae06bd5 100644 --- a/packages/payload-plugin-comments/src/types/index.ts +++ b/packages/payload-plugin-comments/src/types/index.ts @@ -1,4 +1,4 @@ -export type { BaseServiceOptions, BaseDocument } from "./base"; +export type { ServiceContext, BaseDocument } from "./base"; export type { CommentMention, Comment } from "./comment"; export type { CommentRead } from "./commentRead"; export type { diff --git a/packages/payload-plugin-comments/src/utils/payload/extractPayload.ts b/packages/payload-plugin-comments/src/utils/payload/extractPayload.ts deleted file mode 100644 index 8d39da343..000000000 --- a/packages/payload-plugin-comments/src/utils/payload/extractPayload.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { getPayload } from "payload"; -import type { Payload } from "payload"; -import { getPayloadConfig } from "../../config"; - -export async function extractPayload(payload?: Payload): Promise { - return payload ?? (await getPayload({ config: getPayloadConfig() })); -} diff --git a/packages/payload-plugin-comments/tests/endpoints/commentsEndpoints.test.ts b/packages/payload-plugin-comments/tests/endpoints/commentsEndpoints.test.ts new file mode 100644 index 000000000..8d6dd7e67 --- /dev/null +++ b/packages/payload-plugin-comments/tests/endpoints/commentsEndpoints.test.ts @@ -0,0 +1,47 @@ +import type { Payload, PayloadRequest } from "payload"; +import { describe, expect, it, vi } from "vitest"; +import { COMMENTS_ENDPOINT_PATHS } from "../../src/constants"; +import { commentsEndpoints } from "../../src/endpoints"; + +function findEndpoint(path: string) { + const endpoint = commentsEndpoints.find((e) => e.path === path); + + if (!endpoint) throw new Error(`Endpoint ${path} is not registered`); + + return endpoint; +} + +describe("commentsEndpoints", () => { + it("registers every plugin endpoint", () => { + expect(commentsEndpoints.map((e) => e.path).sort()).toEqual( + Object.values(COMMENTS_ENDPOINT_PATHS).sort() + ); + }); + + it("serves comments from the request's Payload on a fresh server instance", async () => { + const comment = { id: 1, text: "hello" }; + const find = vi + .fn() + .mockResolvedValueOnce({ docs: [comment] }) + .mockResolvedValueOnce({ docs: [{ comment: 1 }] }); + const payload = { + config: { admin: { custom: { commentsPlugin: {} } } }, + find, + logger: { error: vi.fn() }, + } as unknown as Payload; + const req = { + user: { id: 7, collection: "users" }, + payload, + headers: new Headers(), + json: async () => ({ docId: "3", filterCollectionSlug: "pages" }), + } as unknown as PayloadRequest; + + const res = await findEndpoint(COMMENTS_ENDPOINT_PATHS.list).handler(req); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ + success: true, + data: [{ ...comment, isReadByCurrentUser: true }], + }); + }); +}); diff --git a/packages/payload-plugin-comments/tests/endpoints/defineEndpoint.test.ts b/packages/payload-plugin-comments/tests/endpoints/defineEndpoint.test.ts new file mode 100644 index 000000000..943bbd477 --- /dev/null +++ b/packages/payload-plugin-comments/tests/endpoints/defineEndpoint.test.ts @@ -0,0 +1,89 @@ +import type { Payload, PayloadRequest } from "payload"; +import { describe, expect, it, vi } from "vitest"; +import { defineEndpoint } from "../../src/endpoints/defineEndpoint"; + +const USER = { id: 7, collection: "users" }; + +function makePayload(): Payload { + return { logger: { error: vi.fn() } } as unknown as Payload; +} + +function makeRequest(overrides: Partial = {}): PayloadRequest { + return { + user: USER, + payload: makePayload(), + headers: new Headers({ cookie: "payload-tenant=1" }), + json: async () => ({ id: 42 }), + ...overrides, + } as unknown as PayloadRequest; +} + +describe("defineEndpoint", () => { + it("registers a POST endpoint on the given path", () => { + const endpoint = defineEndpoint("/comments-plugin/test", vi.fn()); + + expect(endpoint.path).toBe("/comments-plugin/test"); + expect(endpoint.method).toBe("post"); + }); + + it("rejects unauthenticated requests without calling the service", async () => { + const service = vi.fn(); + const endpoint = defineEndpoint("/comments-plugin/test", service); + + const res = await endpoint.handler(makeRequest({ user: null })); + + expect(res.status).toBe(401); + expect(await res.json()).toEqual({ success: false, error: "Unauthorized" }); + expect(service).not.toHaveBeenCalled(); + }); + + it("passes the request payload, user, headers and body to the service", async () => { + const service = vi.fn().mockResolvedValue({ success: true, data: { ok: true } }); + const endpoint = defineEndpoint("/comments-plugin/test", service); + const req = makeRequest(); + + const res = await endpoint.handler(req); + + expect(service).toHaveBeenCalledWith( + { payload: req.payload, user: USER, headers: req.headers }, + { id: 42 } + ); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ success: true, data: { ok: true } }); + }); + + it("responds with 400 without calling the service when the body is not JSON", async () => { + const service = vi.fn(); + const endpoint = defineEndpoint("/comments-plugin/test", service); + + const res = await endpoint.handler( + makeRequest({ json: () => Promise.reject(new SyntaxError("Unexpected token")) }) + ); + + expect(res.status).toBe(400); + expect(await res.json()).toEqual({ success: false, error: "Invalid JSON body" }); + expect(service).not.toHaveBeenCalled(); + }); + + it("responds with 400 when the service reports a failure", async () => { + const service = vi.fn().mockResolvedValue({ success: false, error: "Not mentioned" }); + const endpoint = defineEndpoint("/comments-plugin/test", service); + + const res = await endpoint.handler(makeRequest()); + + expect(res.status).toBe(400); + expect(await res.json()).toEqual({ success: false, error: "Not mentioned" }); + }); + + it("responds with 500 and logs when the service throws", async () => { + const service = vi.fn().mockRejectedValue(new Error("boom")); + const endpoint = defineEndpoint("/comments-plugin/test", service); + const req = makeRequest(); + + const res = await endpoint.handler(req); + + expect(res.status).toBe(500); + expect(await res.json()).toEqual({ success: false, error: "boom" }); + expect(req.payload.logger.error).toHaveBeenCalled(); + }); +});