From fde58f2d3d3f6ea5eb6a37cfa36f7b3cde585f97 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Mon, 4 May 2026 22:32:53 +0200 Subject: [PATCH] feat(blog): add tRPC router with articleBySlug + listArticles + createArticle --- .../blog/src/integrations/api/router.test.ts | 51 +++++++++++++++++++ packages/blog/src/integrations/api/router.ts | 39 ++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 packages/blog/src/integrations/api/router.test.ts create mode 100644 packages/blog/src/integrations/api/router.ts diff --git a/packages/blog/src/integrations/api/router.test.ts b/packages/blog/src/integrations/api/router.test.ts new file mode 100644 index 0000000..7bf8187 --- /dev/null +++ b/packages/blog/src/integrations/api/router.test.ts @@ -0,0 +1,51 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { blogContainer } from "@/di/container"; +import { BLOG_SYMBOLS } from "@/di/symbols"; +import { MockArticlesRepository } from "@/infrastructure/repositories/mock-articles.repository"; +import type { IArticlesRepository } from "@/application/repositories/articles-repository.interface"; +import { blogRouter } from "./router"; + +describe("blogRouter", () => { + let repo: MockArticlesRepository; + + beforeEach(() => { + if (blogContainer.isBound(BLOG_SYMBOLS.IArticlesRepository)) { + blogContainer.unbind(BLOG_SYMBOLS.IArticlesRepository); + } + repo = new MockArticlesRepository(); + blogContainer + .bind(BLOG_SYMBOLS.IArticlesRepository) + .toConstantValue(repo); + }); + + it("exposes articleBySlug, listArticles, createArticle procedures", () => { + const procedureNames = Object.keys(blogRouter._def.procedures); + expect(procedureNames).toContain("articleBySlug"); + expect(procedureNames).toContain("listArticles"); + expect(procedureNames).toContain("createArticle"); + }); + + it("articleBySlug returns the article when present", async () => { + const now = new Date(); + await repo.createArticle({ + id: "1", + title: "T", + slug: "t", + content: null, + status: "draft", + authorId: "u1", + createdAt: now, + updatedAt: now, + }); + + const caller = blogRouter.createCaller({}); + const result = await caller.articleBySlug({ slug: "t" }); + expect(result?.id).toBe("1"); + }); + + it("listArticles returns all articles when no input is given", async () => { + const caller = blogRouter.createCaller({}); + const result = await caller.listArticles(); + expect(result).toEqual([]); + }); +}); diff --git a/packages/blog/src/integrations/api/router.ts b/packages/blog/src/integrations/api/router.ts new file mode 100644 index 0000000..a25bac8 --- /dev/null +++ b/packages/blog/src/integrations/api/router.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; +import { router, publicProcedure } from "@repo/core-shared/trpc/init"; +import { + createArticleController, + getArticlesController, + getArticleBySlugController, +} from "@/interface-adapters/controllers/articles.controller"; + +export const blogRouter = router({ + articleBySlug: publicProcedure + .input(z.object({ slug: z.string().min(1) })) + .query(({ input }) => getArticleBySlugController(input)), + + listArticles: publicProcedure + .input( + z + .object({ + status: z.string().optional(), + authorId: z.string().optional(), + limit: z.number().optional(), + offset: z.number().optional(), + }) + .optional(), + ) + .query(({ input }) => getArticlesController(input ?? {})), + + createArticle: publicProcedure + .input( + z.object({ + title: z.string().min(1).max(255), + content: z.unknown().optional(), + authorId: z.string(), + slug: z.string().optional(), + }), + ) + .mutation(({ input }) => createArticleController(input)), +}); + +export type BlogRouter = typeof blogRouter;