From e6560bc9cb482e70924443bc37c356995138d80e Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 6 May 2026 19:00:31 +0200 Subject: [PATCH] feat(blog): add bind-dev-seed binder + dev seed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sets the canonical pattern for all 5 features. Three new files: - src/__seeds__/dev.ts — buildDevArticles() function returning 3 realistic articles (welcome, vertical-feature-architecture, wip-post) built from articleFactory with id/slug/title/status overrides only. - src/di/bind-dev-seed.ts — bindDevSeedBlog() async function that unbinds IArticlesRepository, constructs MockArticlesRepository, seeds it via buildDevArticles(), and rebinds via .toConstantValue. - src/di/bind-dev-seed.test.ts — 3 tests: populated repo, welcome article reachable by slug, idempotent (callable twice). package.json adds the ./di/bind-dev-seed subpath export, parallel to ./di/bind-production. Tests + use cases continue to construct MockArticlesRepository directly — they never go through bindDevSeedBlog. The seed only attaches when called explicitly at app boot. --- packages/blog/package.json | 3 +- packages/blog/src/__seeds__/dev.ts | 35 +++++++++++ packages/blog/src/di/bind-dev-seed.test.ts | 71 ++++++++++++++++++++++ packages/blog/src/di/bind-dev-seed.ts | 30 +++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 packages/blog/src/__seeds__/dev.ts create mode 100644 packages/blog/src/di/bind-dev-seed.test.ts create mode 100644 packages/blog/src/di/bind-dev-seed.ts diff --git a/packages/blog/package.json b/packages/blog/package.json index dc2fcf1..48490ff 100644 --- a/packages/blog/package.json +++ b/packages/blog/package.json @@ -8,7 +8,8 @@ "./ui": "./src/ui/index.ts", "./cms": "./src/integrations/cms/index.ts", "./api": "./src/integrations/api/router.ts", - "./di/bind-production": "./src/di/bind-production.ts" + "./di/bind-production": "./src/di/bind-production.ts", + "./di/bind-dev-seed": "./src/di/bind-dev-seed.ts" }, "scripts": { "build": "tsc --noEmit", diff --git a/packages/blog/src/__seeds__/dev.ts b/packages/blog/src/__seeds__/dev.ts new file mode 100644 index 0000000..72e2215 --- /dev/null +++ b/packages/blog/src/__seeds__/dev.ts @@ -0,0 +1,35 @@ +import { articleFactory } from "../__factories__/article.factory.js"; +import type { Article } from "../entities/models/article.js"; + +/** + * Realistic blog seed for dev mode + storybook stories. + * + * Built from `articleFactory` so factory defaults take care of the boring + * fields (createdAt, updatedAt, content, authorId) and we only override what + * makes the data look like a populated database. + * + * Lazily produced so importing this module is side-effect-free — the factory's + * sequence counter only advances when a binder calls `buildDevArticles()`. + */ +export function buildDevArticles(): Article[] { + return [ + articleFactory.build({ + id: "welcome", + slug: "welcome", + title: "Welcome to the blog", + status: "published", + }), + articleFactory.build({ + id: "vertical-feature-architecture", + slug: "vertical-feature-architecture", + title: "Why vertical-feature packages", + status: "published", + }), + articleFactory.build({ + id: "wip-post", + slug: "work-in-progress", + title: "A draft we haven't shipped yet", + status: "draft", + }), + ]; +} diff --git a/packages/blog/src/di/bind-dev-seed.test.ts b/packages/blog/src/di/bind-dev-seed.test.ts new file mode 100644 index 0000000..fef92da --- /dev/null +++ b/packages/blog/src/di/bind-dev-seed.test.ts @@ -0,0 +1,71 @@ +import "reflect-metadata"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { bindDevSeedBlog } from "@/di/bind-dev-seed"; +import { blogContainer } from "@/di/container"; +import { BLOG_SYMBOLS } from "@/di/symbols"; +import { MockArticlesRepository } from "@/infrastructure/repositories/articles.repository.mock"; +import type { IArticlesRepository } from "@/application/repositories/articles.repository.interface"; + +describe("bindDevSeedBlog", () => { + // Each test starts from the default empty-mock binding and tears down + // afterwards so the global blogContainer state stays clean for siblings. + beforeEach(() => { + if (blogContainer.isBound(BLOG_SYMBOLS.IArticlesRepository)) { + blogContainer.unbind(BLOG_SYMBOLS.IArticlesRepository); + } + blogContainer + .bind(BLOG_SYMBOLS.IArticlesRepository) + .to(MockArticlesRepository); + }); + + afterEach(() => { + if (blogContainer.isBound(BLOG_SYMBOLS.IArticlesRepository)) { + blogContainer.unbind(BLOG_SYMBOLS.IArticlesRepository); + } + blogContainer + .bind(BLOG_SYMBOLS.IArticlesRepository) + .to(MockArticlesRepository); + }); + + it("populates the repository with the dev articles", async () => { + await bindDevSeedBlog(); + + const repo = blogContainer.get( + BLOG_SYMBOLS.IArticlesRepository, + ); + const all = await repo.getArticles(); + + expect(all.length).toBeGreaterThan(0); + }); + + it("seeds the welcome article reachable by slug", async () => { + await bindDevSeedBlog(); + + const repo = blogContainer.get( + BLOG_SYMBOLS.IArticlesRepository, + ); + const welcome = await repo.getArticleBySlug("welcome"); + + expect(welcome).toBeDefined(); + expect(welcome?.title).toBe("Welcome to the blog"); + expect(welcome?.status).toBe("published"); + }); + + it("is idempotent — calling twice rebuilds a fresh populated repo", async () => { + await bindDevSeedBlog(); + const before = blogContainer.get( + BLOG_SYMBOLS.IArticlesRepository, + ); + const beforeCount = (await before.getArticles()).length; + + await bindDevSeedBlog(); + const after = blogContainer.get( + BLOG_SYMBOLS.IArticlesRepository, + ); + const afterCount = (await after.getArticles()).length; + + expect(afterCount).toBe(beforeCount); + // It's a fresh instance — not the previous one. + expect(after).not.toBe(before); + }); +}); diff --git a/packages/blog/src/di/bind-dev-seed.ts b/packages/blog/src/di/bind-dev-seed.ts new file mode 100644 index 0000000..9fa177e --- /dev/null +++ b/packages/blog/src/di/bind-dev-seed.ts @@ -0,0 +1,30 @@ +import { blogContainer } from "./container.js"; +import { BLOG_SYMBOLS } from "./symbols.js"; +import { MockArticlesRepository } from "../infrastructure/repositories/articles.repository.mock.js"; +import { buildDevArticles } from "../__seeds__/dev.js"; +import type { IArticlesRepository } from "../application/repositories/articles.repository.interface.js"; + +/** + * Replace the default empty mock with a populated one for dev mode + storybook. + * + * Call this from app boot when `USE_DEV_SEED=true`, mutually exclusive with + * `bindProductionBlog(config)`. Tests must NOT call this — they construct + * `new MockArticlesRepository()` directly and seed via factories per-test. + * + * Idempotent: safe to call multiple times; each call rebuilds a fresh + * populated repo and rebinds the symbol. + */ +export async function bindDevSeedBlog(): Promise { + if (blogContainer.isBound(BLOG_SYMBOLS.IArticlesRepository)) { + blogContainer.unbind(BLOG_SYMBOLS.IArticlesRepository); + } + + const repo = new MockArticlesRepository(); + for (const article of buildDevArticles()) { + await repo.createArticle(article); + } + + blogContainer + .bind(BLOG_SYMBOLS.IArticlesRepository) + .toConstantValue(repo); +}