import "reflect-metadata"; import { injectable } from "inversify"; import { getPayload } from "payload"; import type { SanitizedConfig } from "payload"; import { NoopTracer, NoopLogger, type ITracer, type ILogger, } from "@repo/core-shared/instrumentation"; import type { IArticlesRepository } from "../../application/repositories/articles.repository.interface"; import type { Article } from "../../entities/models/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), }; } const FEATURE = "blog" as const; const REPO = "articles" as const; @injectable() export class ArticlesRepository implements IArticlesRepository { private config: SanitizedConfig; private tracer: ITracer; private logger: ILogger; constructor( config: SanitizedConfig, tracer: ITracer = new NoopTracer(), logger: ILogger = new NoopLogger(), ) { this.config = config; this.tracer = tracer; this.logger = logger; } async getArticle(id: string): Promise
{ return this.tracer.startSpan( { name: "articles.getArticle", op: "repository", attributes: { id } }, async (span) => { try { const payload = await getPayload({ config: this.config }); const doc = await payload.findByID({ collection: "articles", id, overrideAccess: true, }); span.setAttribute("found", true); return mapDoc(doc as PayloadArticleDoc); } catch (err) { // Payload throws on not-found; treat as undefined per existing semantics if ( err && typeof err === "object" && "status" in err && (err as { status: unknown }).status === 404 ) { span.setAttribute("found", false); return undefined; } this.logger.captureException(err, { tags: { feature: FEATURE, repo: REPO, method: "getArticle" }, }); span.setStatus( "error", err instanceof Error ? err.message : String(err), ); throw err; } }, ); } async getArticleBySlug(slug: string): Promise
{ return this.tracer.startSpan( { name: "articles.getArticleBySlug", op: "repository", attributes: { slug }, }, async (span) => { try { const payload = await getPayload({ config: this.config }); const result = await payload.find({ collection: "articles", where: { slug: { equals: slug } }, limit: 1, overrideAccess: true, }); const doc = result.docs[0] as PayloadArticleDoc | undefined; span.setAttribute("found", Boolean(doc)); return doc ? mapDoc(doc) : undefined; } catch (err) { this.logger.captureException(err, { tags: { feature: FEATURE, repo: REPO, method: "getArticleBySlug" }, }); span.setStatus( "error", err instanceof Error ? err.message : String(err), ); throw err; } }, ); } async getArticles(options?: { status?: string; authorId?: string; limit?: number; offset?: number; }): Promise { return this.tracer.startSpan( { name: "articles.getArticles", op: "repository", attributes: { status: options?.status ?? null, authorId: options?.authorId ?? null, limit: options?.limit ?? null, offset: options?.offset ?? null, }, }, async (span) => { try { 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 }; const limit = options?.limit ?? 50; const offset = options?.offset ?? 0; // Payload paginates by page, not offset. A non-aligned offset // (offset % limit !== 0) straddles two pages, so fetch both and // slice out the exact [offset, offset + limit) window. const page = limit > 0 ? Math.floor(offset / limit) + 1 : 1; const remainder = limit > 0 ? offset % limit : 0; const findPage = (p: number) => payload.find({ collection: "articles", where: where as never, limit, page: p, overrideAccess: true, }); const first = await findPage(page); let docs = first.docs; if (remainder > 0) { if (docs.length === limit) { const second = await findPage(page + 1); docs = [...docs, ...second.docs]; } docs = docs.slice(remainder, remainder + limit); } span.setAttribute("count", docs.length); return docs.map((d) => mapDoc(d as PayloadArticleDoc)); } catch (err) { this.logger.captureException(err, { tags: { feature: FEATURE, repo: REPO, method: "getArticles" }, }); span.setStatus( "error", err instanceof Error ? err.message : String(err), ); throw err; } }, ); } async createArticle(input: Article): Promise
{ return this.tracer.startSpan( { name: "articles.createArticle", op: "repository", attributes: { slug: input.slug }, }, async (span) => { try { const payload = await getPayload({ config: this.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: true, }); span.setAttribute("id", String((created as PayloadArticleDoc).id)); return mapDoc(created as PayloadArticleDoc); } catch (err) { this.logger.captureException(err, { tags: { feature: FEATURE, repo: REPO, method: "createArticle" }, }); span.setStatus( "error", err instanceof Error ? err.message : String(err), ); throw err; } }, ); } async updateArticle( id: string, input: Partial
, ): Promise
{ return this.tracer.startSpan( { name: "articles.updateArticle", op: "repository", attributes: { id } }, async (span) => { try { const payload = await getPayload({ config: this.config }); 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: true, }); span.setAttribute("found", true); return mapDoc(updated as PayloadArticleDoc); } catch (err) { if ( err && typeof err === "object" && "status" in err && (err as { status: unknown }).status === 404 ) { span.setAttribute("found", false); return undefined; } this.logger.captureException(err, { tags: { feature: FEATURE, repo: REPO, method: "updateArticle" }, }); span.setStatus( "error", err instanceof Error ? err.message : String(err), ); throw err; } }, ); } }