feat(core-cms): compose @repo/blog/cms into payload config

This commit is contained in:
2026-05-04 22:29:50 +02:00
parent 1d59698ebe
commit a92341fb74
9 changed files with 93 additions and 24 deletions

View File

@@ -15,7 +15,6 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@repo/core-cms": "workspace:*",
"@repo/core-shared": "workspace:*", "@repo/core-shared": "workspace:*",
"@trpc/server": "^11.0.0", "@trpc/server": "^11.0.0",
"inversify": "^6.2.0", "inversify": "^6.2.0",

View File

@@ -12,7 +12,7 @@ function generateSlug(title: string): string {
export async function createArticleUseCase(input: { export async function createArticleUseCase(input: {
title: string; title: string;
content: unknown; content?: unknown;
authorId: string; authorId: string;
slug?: string; slug?: string;
}): Promise<Article> { }): Promise<Article> {

View File

@@ -5,11 +5,9 @@ vi.mock("payload", () => ({
getPayload: vi.fn(), getPayload: vi.fn(),
})); }));
vi.mock("@repo/core-cms", () => ({
default: {} as never,
}));
describe("PayloadArticlesRepository", () => { describe("PayloadArticlesRepository", () => {
const mockConfig = {} as never;
it("maps a Payload doc to a domain Article on getArticleBySlug", async () => { it("maps a Payload doc to a domain Article on getArticleBySlug", async () => {
const { getPayload } = await import("payload"); const { getPayload } = await import("payload");
const findMock = vi.fn().mockResolvedValue({ const findMock = vi.fn().mockResolvedValue({
@@ -30,7 +28,7 @@ describe("PayloadArticlesRepository", () => {
find: findMock, find: findMock,
}); });
const repo = new PayloadArticlesRepository(); const repo = new PayloadArticlesRepository(mockConfig);
const result = await repo.getArticleBySlug("hello"); const result = await repo.getArticleBySlug("hello");
expect(findMock).toHaveBeenCalledWith({ expect(findMock).toHaveBeenCalledWith({
@@ -52,7 +50,7 @@ describe("PayloadArticlesRepository", () => {
find: vi.fn().mockResolvedValue({ docs: [] }), find: vi.fn().mockResolvedValue({ docs: [] }),
}); });
const repo = new PayloadArticlesRepository(); const repo = new PayloadArticlesRepository(mockConfig);
const result = await repo.getArticleBySlug("missing"); const result = await repo.getArticleBySlug("missing");
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });

View File

@@ -1,8 +1,8 @@
import "reflect-metadata"; import "reflect-metadata";
import { injectable } from "inversify"; import { injectable } from "inversify";
import { getPayload } from "payload"; 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 { IArticlesRepository } from "@/application/repositories/articles-repository.interface";
import type { Article } from "@/entities/article"; import type { Article } from "@/entities/article";
@@ -38,8 +38,14 @@ function mapDoc(doc: PayloadArticleDoc): Article {
@injectable() @injectable()
export class PayloadArticlesRepository implements IArticlesRepository { export class PayloadArticlesRepository implements IArticlesRepository {
private config: SanitizedConfig;
constructor(config: SanitizedConfig) {
this.config = config;
}
async getArticle(id: string): Promise<Article | undefined> { async getArticle(id: string): Promise<Article | undefined> {
const payload = await getPayload({ config }); const payload = await getPayload({ config: this.config });
try { try {
const doc = await payload.findByID({ const doc = await payload.findByID({
collection: "articles", collection: "articles",
@@ -53,7 +59,7 @@ export class PayloadArticlesRepository implements IArticlesRepository {
} }
async getArticleBySlug(slug: string): Promise<Article | undefined> { async getArticleBySlug(slug: string): Promise<Article | undefined> {
const payload = await getPayload({ config }); const payload = await getPayload({ config: this.config });
const result = await payload.find({ const result = await payload.find({
collection: "articles", collection: "articles",
where: { slug: { equals: slug } }, where: { slug: { equals: slug } },
@@ -70,7 +76,7 @@ export class PayloadArticlesRepository implements IArticlesRepository {
limit?: number; limit?: number;
offset?: number; offset?: number;
}): Promise<Article[]> { }): Promise<Article[]> {
const payload = await getPayload({ config }); const payload = await getPayload({ config: this.config });
const where: Record<string, { equals: string }> = {}; const where: Record<string, { equals: string }> = {};
if (options?.status) where.status = { equals: options.status }; if (options?.status) where.status = { equals: options.status };
if (options?.authorId) where.author = { equals: options.authorId }; if (options?.authorId) where.author = { equals: options.authorId };
@@ -88,7 +94,7 @@ export class PayloadArticlesRepository implements IArticlesRepository {
} }
async createArticle(input: Article): Promise<Article> { async createArticle(input: Article): Promise<Article> {
const payload = await getPayload({ config }); const payload = await getPayload({ config: this.config });
const created = await payload.create({ const created = await payload.create({
collection: "articles", collection: "articles",
data: { data: {
@@ -107,7 +113,7 @@ export class PayloadArticlesRepository implements IArticlesRepository {
id: string, id: string,
input: Partial<Article>, input: Partial<Article>,
): Promise<Article | undefined> { ): Promise<Article | undefined> {
const payload = await getPayload({ config }); const payload = await getPayload({ config: this.config });
try { try {
const updated = await payload.update({ const updated = await payload.update({
collection: "articles", collection: "articles",

View File

@@ -10,7 +10,7 @@ import { createArticleUseCase } from "@/application/use-cases/create-article.use
const createInputSchema = z.object({ const createInputSchema = z.object({
title: z.string().min(1).max(255), title: z.string().min(1).max(255),
content: z.unknown(), content: z.unknown().optional(),
authorId: z.string(), authorId: z.string(),
slug: z.string().optional(), slug: z.string().optional(),
}); });
@@ -35,7 +35,12 @@ export async function createArticleController(
cause: parsed.error, 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( export async function getArticlesController(

View File

@@ -13,6 +13,7 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@repo/blog": "workspace:*",
"payload": "^3.14.0", "payload": "^3.14.0",
"@payloadcms/db-postgres": "^3.14.0", "@payloadcms/db-postgres": "^3.14.0",
"@payloadcms/richtext-lexical": "^3.14.0" "@payloadcms/richtext-lexical": "^3.14.0"

View File

@@ -67,6 +67,7 @@ export interface Config {
}; };
blocks: {}; blocks: {};
collections: { collections: {
articles: Article;
'payload-kv': PayloadKv; 'payload-kv': PayloadKv;
users: User; users: User;
'payload-locked-documents': PayloadLockedDocument; 'payload-locked-documents': PayloadLockedDocument;
@@ -75,6 +76,7 @@ export interface Config {
}; };
collectionsJoins: {}; collectionsJoins: {};
collectionsSelect: { collectionsSelect: {
articles: ArticlesSelect<false> | ArticlesSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>; 'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
users: UsersSelect<false> | UsersSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>; 'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
@@ -115,6 +117,42 @@ export interface UserAuthOperations {
password: string; 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 * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv". * via the `definition` "payload-kv".
@@ -163,10 +201,15 @@ export interface User {
*/ */
export interface PayloadLockedDocument { export interface PayloadLockedDocument {
id: number; id: number;
document?: { document?:
| ({
relationTo: 'articles';
value: number | Article;
} | null)
| ({
relationTo: 'users'; relationTo: 'users';
value: number | User; value: number | User;
} | null; } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
relationTo: 'users'; relationTo: 'users';
@@ -209,6 +252,21 @@ export interface PayloadMigration {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "articles_select".
*/
export interface ArticlesSelect<T extends boolean = true> {
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 * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv_select". * via the `definition` "payload-kv_select".

View File

@@ -4,12 +4,14 @@ import { lexicalEditor } from "@payloadcms/richtext-lexical";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { articles } from "@repo/blog/cms";
const filename = fileURLToPath(import.meta.url); const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename); const dirname = path.dirname(filename);
export default buildConfig({ export default buildConfig({
editor: lexicalEditor(), editor: lexicalEditor(),
collections: [], collections: [articles],
globals: [], globals: [],
secret: process.env.PAYLOAD_SECRET || "default-secret-change-me", secret: process.env.PAYLOAD_SECRET || "default-secret-change-me",
db: postgresAdapter({ db: postgresAdapter({

6
pnpm-lock.yaml generated
View File

@@ -230,9 +230,6 @@ importers:
packages/blog: packages/blog:
dependencies: dependencies:
'@repo/core-cms':
specifier: workspace:*
version: link:../core-cms
'@repo/core-shared': '@repo/core-shared':
specifier: workspace:* specifier: workspace:*
version: link:../core-shared version: link:../core-shared
@@ -355,6 +352,9 @@ importers:
'@payloadcms/richtext-lexical': '@payloadcms/richtext-lexical':
specifier: ^3.14.0 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) 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: payload:
specifier: ^3.14.0 specifier: ^3.14.0
version: 3.81.0(graphql@16.13.2)(typescript@5.9.3) version: 3.81.0(graphql@16.13.2)(typescript@5.9.3)