diff --git a/packages/blog/src/infrastructure/repositories/payload-articles.repository.test.ts b/packages/blog/src/infrastructure/repositories/payload-articles.repository.test.ts new file mode 100644 index 0000000..658dc52 --- /dev/null +++ b/packages/blog/src/infrastructure/repositories/payload-articles.repository.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from "vitest"; +import { PayloadArticlesRepository } from "./payload-articles.repository"; + +vi.mock("payload", () => ({ + getPayload: vi.fn(), +})); + +vi.mock("@repo/core-cms", () => ({ + default: {} as never, +})); + +describe("PayloadArticlesRepository", () => { + it("maps a Payload doc to a domain Article on getArticleBySlug", async () => { + const { getPayload } = await import("payload"); + const findMock = vi.fn().mockResolvedValue({ + docs: [ + { + id: "p-123", + title: "Hello", + slug: "hello", + content: { type: "doc", children: [] }, + status: "published", + author: "u1", + createdAt: "2026-05-04T12:00:00.000Z", + updatedAt: "2026-05-04T12:00:00.000Z", + }, + ], + }); + (getPayload as ReturnType).mockResolvedValue({ + find: findMock, + }); + + const repo = new PayloadArticlesRepository(); + const result = await repo.getArticleBySlug("hello"); + + expect(findMock).toHaveBeenCalledWith({ + collection: "articles", + where: { slug: { equals: "hello" } }, + limit: 1, + overrideAccess: false, + }); + expect(result?.id).toBe("p-123"); + expect(result?.slug).toBe("hello"); + expect(result?.status).toBe("published"); + expect(result?.authorId).toBe("u1"); + expect(result?.createdAt).toBeInstanceOf(Date); + }); + + it("returns undefined when slug is not found", async () => { + const { getPayload } = await import("payload"); + (getPayload as ReturnType).mockResolvedValue({ + find: vi.fn().mockResolvedValue({ docs: [] }), + }); + + const repo = new PayloadArticlesRepository(); + const result = await repo.getArticleBySlug("missing"); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/blog/src/infrastructure/repositories/payload-articles.repository.ts b/packages/blog/src/infrastructure/repositories/payload-articles.repository.ts new file mode 100644 index 0000000..a7a8409 --- /dev/null +++ b/packages/blog/src/infrastructure/repositories/payload-articles.repository.ts @@ -0,0 +1,129 @@ +import "reflect-metadata"; +import { injectable } from "inversify"; +import { getPayload } from "payload"; + +import config from "@repo/core-cms"; +import type { IArticlesRepository } from "@/application/repositories/articles-repository.interface"; +import type { Article } from "@/entities/article"; + +type PayloadArticleDoc = { + id: string | number; + title?: string | null; + slug?: string | null; + content?: unknown; + status?: string | null; + author?: string | number | { id: string | number } | null; + createdAt?: string | null; + updatedAt?: string | null; +}; + +function mapDoc(doc: PayloadArticleDoc): Article { + const authorId = + typeof doc.author === "object" && doc.author !== null + ? String(doc.author.id) + : doc.author != null + ? String(doc.author) + : ""; + return { + id: String(doc.id), + title: doc.title ?? "", + slug: doc.slug ?? "", + content: doc.content ?? null, + status: doc.status === "published" ? "published" : "draft", + authorId, + createdAt: doc.createdAt ? new Date(doc.createdAt) : new Date(0), + updatedAt: doc.updatedAt ? new Date(doc.updatedAt) : new Date(0), + }; +} + +@injectable() +export class PayloadArticlesRepository implements IArticlesRepository { + async getArticle(id: string): Promise
{ + const payload = await getPayload({ config }); + try { + const doc = await payload.findByID({ + collection: "articles", + id, + overrideAccess: false, + }); + return mapDoc(doc as PayloadArticleDoc); + } catch { + return undefined; + } + } + + async getArticleBySlug(slug: string): Promise
{ + const payload = await getPayload({ config }); + const result = await payload.find({ + collection: "articles", + where: { slug: { equals: slug } }, + limit: 1, + overrideAccess: false, + }); + const doc = result.docs[0] as PayloadArticleDoc | undefined; + return doc ? mapDoc(doc) : undefined; + } + + async getArticles(options?: { + status?: string; + authorId?: string; + limit?: number; + offset?: number; + }): Promise { + const payload = await getPayload({ config }); + const where: Record = {}; + if (options?.status) where.status = { equals: options.status }; + if (options?.authorId) where.author = { equals: options.authorId }; + + const result = await payload.find({ + collection: "articles", + where: where as never, + limit: options?.limit ?? 50, + page: options?.offset + ? Math.floor(options.offset / (options.limit ?? 50)) + 1 + : 1, + overrideAccess: false, + }); + return result.docs.map((d) => mapDoc(d as PayloadArticleDoc)); + } + + async createArticle(input: Article): Promise
{ + const payload = await getPayload({ config }); + const created = await payload.create({ + collection: "articles", + data: { + title: input.title, + slug: input.slug, + content: input.content, + status: input.status, + author: input.authorId, + } as never, + overrideAccess: false, + }); + return mapDoc(created as PayloadArticleDoc); + } + + async updateArticle( + id: string, + input: Partial
, + ): Promise
{ + const payload = await getPayload({ config }); + try { + const updated = await payload.update({ + collection: "articles", + id, + data: { + ...(input.title !== undefined && { title: input.title }), + ...(input.slug !== undefined && { slug: input.slug }), + ...(input.content !== undefined && { content: input.content }), + ...(input.status !== undefined && { status: input.status }), + ...(input.authorId !== undefined && { author: input.authorId }), + } as never, + overrideAccess: false, + }); + return mapDoc(updated as PayloadArticleDoc); + } catch { + return undefined; + } + } +}