From a92341fb74180074c7cd702a6b2699e8139f5fb4 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Mon, 4 May 2026 22:29:50 +0200 Subject: [PATCH] feat(core-cms): compose @repo/blog/cms into payload config --- packages/blog/package.json | 1 - .../use-cases/create-article.use-case.ts | 2 +- .../payload-articles.repository.test.ts | 10 ++- .../payload-articles.repository.ts | 18 +++-- .../controllers/articles.controller.ts | 9 ++- packages/core-cms/package.json | 1 + packages/core-cms/src/generated-types.ts | 66 +++++++++++++++++-- packages/core-cms/src/payload.config.ts | 4 +- pnpm-lock.yaml | 6 +- 9 files changed, 93 insertions(+), 24 deletions(-) diff --git a/packages/blog/package.json b/packages/blog/package.json index 32c7877..ba59be2 100644 --- a/packages/blog/package.json +++ b/packages/blog/package.json @@ -15,7 +15,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@repo/core-cms": "workspace:*", "@repo/core-shared": "workspace:*", "@trpc/server": "^11.0.0", "inversify": "^6.2.0", diff --git a/packages/blog/src/application/use-cases/create-article.use-case.ts b/packages/blog/src/application/use-cases/create-article.use-case.ts index 1b77fc5..e55565d 100644 --- a/packages/blog/src/application/use-cases/create-article.use-case.ts +++ b/packages/blog/src/application/use-cases/create-article.use-case.ts @@ -12,7 +12,7 @@ function generateSlug(title: string): string { export async function createArticleUseCase(input: { title: string; - content: unknown; + content?: unknown; authorId: string; slug?: string; }): Promise
{ diff --git a/packages/blog/src/infrastructure/repositories/payload-articles.repository.test.ts b/packages/blog/src/infrastructure/repositories/payload-articles.repository.test.ts index 658dc52..55f5b70 100644 --- a/packages/blog/src/infrastructure/repositories/payload-articles.repository.test.ts +++ b/packages/blog/src/infrastructure/repositories/payload-articles.repository.test.ts @@ -5,11 +5,9 @@ vi.mock("payload", () => ({ getPayload: vi.fn(), })); -vi.mock("@repo/core-cms", () => ({ - default: {} as never, -})); - describe("PayloadArticlesRepository", () => { + const mockConfig = {} as never; + it("maps a Payload doc to a domain Article on getArticleBySlug", async () => { const { getPayload } = await import("payload"); const findMock = vi.fn().mockResolvedValue({ @@ -30,7 +28,7 @@ describe("PayloadArticlesRepository", () => { find: findMock, }); - const repo = new PayloadArticlesRepository(); + const repo = new PayloadArticlesRepository(mockConfig); const result = await repo.getArticleBySlug("hello"); expect(findMock).toHaveBeenCalledWith({ @@ -52,7 +50,7 @@ describe("PayloadArticlesRepository", () => { find: vi.fn().mockResolvedValue({ docs: [] }), }); - const repo = new PayloadArticlesRepository(); + const repo = new PayloadArticlesRepository(mockConfig); 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 index a7a8409..da7912c 100644 --- a/packages/blog/src/infrastructure/repositories/payload-articles.repository.ts +++ b/packages/blog/src/infrastructure/repositories/payload-articles.repository.ts @@ -1,8 +1,8 @@ import "reflect-metadata"; import { injectable } from "inversify"; import { getPayload } from "payload"; +import type { SanitizedConfig } from "payload"; -import config from "@repo/core-cms"; import type { IArticlesRepository } from "@/application/repositories/articles-repository.interface"; import type { Article } from "@/entities/article"; @@ -38,8 +38,14 @@ function mapDoc(doc: PayloadArticleDoc): Article { @injectable() export class PayloadArticlesRepository implements IArticlesRepository { + private config: SanitizedConfig; + + constructor(config: SanitizedConfig) { + this.config = config; + } + async getArticle(id: string): Promise
{ - const payload = await getPayload({ config }); + const payload = await getPayload({ config: this.config }); try { const doc = await payload.findByID({ collection: "articles", @@ -53,7 +59,7 @@ export class PayloadArticlesRepository implements IArticlesRepository { } async getArticleBySlug(slug: string): Promise
{ - const payload = await getPayload({ config }); + const payload = await getPayload({ config: this.config }); const result = await payload.find({ collection: "articles", where: { slug: { equals: slug } }, @@ -70,7 +76,7 @@ export class PayloadArticlesRepository implements IArticlesRepository { limit?: number; offset?: number; }): Promise { - const payload = await getPayload({ config }); + const payload = await getPayload({ config: this.config }); const where: Record = {}; if (options?.status) where.status = { equals: options.status }; if (options?.authorId) where.author = { equals: options.authorId }; @@ -88,7 +94,7 @@ export class PayloadArticlesRepository implements IArticlesRepository { } async createArticle(input: Article): Promise
{ - const payload = await getPayload({ config }); + const payload = await getPayload({ config: this.config }); const created = await payload.create({ collection: "articles", data: { @@ -107,7 +113,7 @@ export class PayloadArticlesRepository implements IArticlesRepository { id: string, input: Partial
, ): Promise
{ - const payload = await getPayload({ config }); + const payload = await getPayload({ config: this.config }); try { const updated = await payload.update({ collection: "articles", diff --git a/packages/blog/src/interface-adapters/controllers/articles.controller.ts b/packages/blog/src/interface-adapters/controllers/articles.controller.ts index 5d04e29..2aaefd8 100644 --- a/packages/blog/src/interface-adapters/controllers/articles.controller.ts +++ b/packages/blog/src/interface-adapters/controllers/articles.controller.ts @@ -10,7 +10,7 @@ import { createArticleUseCase } from "@/application/use-cases/create-article.use const createInputSchema = z.object({ title: z.string().min(1).max(255), - content: z.unknown(), + content: z.unknown().optional(), authorId: z.string(), slug: z.string().optional(), }); @@ -35,7 +35,12 @@ export async function createArticleController( cause: parsed.error, }); } - return createArticleUseCase(parsed.data); + return createArticleUseCase({ + title: parsed.data.title, + content: parsed.data.content ?? null, + authorId: parsed.data.authorId, + slug: parsed.data.slug, + }); } export async function getArticlesController( diff --git a/packages/core-cms/package.json b/packages/core-cms/package.json index 279b453..af0b774 100644 --- a/packages/core-cms/package.json +++ b/packages/core-cms/package.json @@ -13,6 +13,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@repo/blog": "workspace:*", "payload": "^3.14.0", "@payloadcms/db-postgres": "^3.14.0", "@payloadcms/richtext-lexical": "^3.14.0" diff --git a/packages/core-cms/src/generated-types.ts b/packages/core-cms/src/generated-types.ts index 703fa14..520f18e 100644 --- a/packages/core-cms/src/generated-types.ts +++ b/packages/core-cms/src/generated-types.ts @@ -67,6 +67,7 @@ export interface Config { }; blocks: {}; collections: { + articles: Article; 'payload-kv': PayloadKv; users: User; 'payload-locked-documents': PayloadLockedDocument; @@ -75,6 +76,7 @@ export interface Config { }; collectionsJoins: {}; collectionsSelect: { + articles: ArticlesSelect | ArticlesSelect; 'payload-kv': PayloadKvSelect | PayloadKvSelect; users: UsersSelect | UsersSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; @@ -115,6 +117,42 @@ export interface UserAuthOperations { password: string; }; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "articles". + */ +export interface Article { + id: number; + title: string; + /** + * Auto-generated from title if left empty + */ + slug?: string | null; + content?: { + root: { + type: string; + children: { + type: any; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + status: 'draft' | 'published'; + /** + * Temporary text field; restored to users relationship in Plan 3. + */ + author: string; + publishedAt?: string | null; + updatedAt: string; + createdAt: string; + _status?: ('draft' | 'published') | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-kv". @@ -163,10 +201,15 @@ export interface User { */ export interface PayloadLockedDocument { id: number; - document?: { - relationTo: 'users'; - value: number | User; - } | null; + document?: + | ({ + relationTo: 'articles'; + value: number | Article; + } | null) + | ({ + relationTo: 'users'; + value: number | User; + } | null); globalSlug?: string | null; user: { relationTo: 'users'; @@ -209,6 +252,21 @@ export interface PayloadMigration { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "articles_select". + */ +export interface ArticlesSelect { + title?: T; + slug?: T; + content?: T; + status?: T; + author?: T; + publishedAt?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-kv_select". diff --git a/packages/core-cms/src/payload.config.ts b/packages/core-cms/src/payload.config.ts index b166388..c77e2e9 100644 --- a/packages/core-cms/src/payload.config.ts +++ b/packages/core-cms/src/payload.config.ts @@ -4,12 +4,14 @@ import { lexicalEditor } from "@payloadcms/richtext-lexical"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { articles } from "@repo/blog/cms"; + const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); export default buildConfig({ editor: lexicalEditor(), - collections: [], + collections: [articles], globals: [], secret: process.env.PAYLOAD_SECRET || "default-secret-change-me", db: postgresAdapter({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c3b9cc..6c5a54f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -230,9 +230,6 @@ importers: packages/blog: dependencies: - '@repo/core-cms': - specifier: workspace:* - version: link:../core-cms '@repo/core-shared': specifier: workspace:* version: link:../core-shared @@ -355,6 +352,9 @@ importers: '@payloadcms/richtext-lexical': specifier: ^3.14.0 version: 3.81.0(@faceless-ui/modal@3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@payloadcms/next@3.81.0(graphql@16.13.2)(next@16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.99.0))(payload@3.81.0(graphql@16.13.2)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(next@16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.99.0))(payload@3.81.0(graphql@16.13.2)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(yjs@13.6.30) + '@repo/blog': + specifier: workspace:* + version: link:../blog payload: specifier: ^3.14.0 version: 3.81.0(graphql@16.13.2)(typescript@5.9.3)