diff --git a/packages/blog/src/entities/article.test.ts b/packages/blog/src/entities/article.test.ts new file mode 100644 index 0000000..e5e9727 --- /dev/null +++ b/packages/blog/src/entities/article.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import { articleSchema, articleStatusSchema, type Article } from "./article"; + +describe("articleSchema", () => { + it("accepts a minimal valid article with default status", () => { + const result = articleSchema.parse({ + id: "abc", + title: "Hello", + slug: "hello", + content: { type: "doc", children: [] }, + authorId: "u1", + createdAt: new Date(), + updatedAt: new Date(), + }); + expect(result.status).toBe("draft"); + }); + + it("accepts unknown rich-text content", () => { + const result = articleSchema.parse({ + id: "abc", + title: "Hello", + slug: "hello", + content: "any string is also fine", + authorId: "u1", + createdAt: new Date(), + updatedAt: new Date(), + }); + expect(result.content).toBe("any string is also fine"); + }); + + it("rejects empty title", () => { + expect(() => + articleSchema.parse({ + id: "a", + title: "", + slug: "s", + content: null, + authorId: "u", + createdAt: new Date(), + updatedAt: new Date(), + }), + ).toThrow(); + }); + + it("rejects title over 255 chars", () => { + expect(() => + articleSchema.parse({ + id: "a", + title: "x".repeat(256), + slug: "s", + content: null, + authorId: "u", + createdAt: new Date(), + updatedAt: new Date(), + }), + ).toThrow(); + }); +}); + +describe("articleStatusSchema", () => { + it("accepts 'draft' and 'published'", () => { + expect(articleStatusSchema.parse("draft")).toBe("draft"); + expect(articleStatusSchema.parse("published")).toBe("published"); + }); + + it("rejects unknown status", () => { + expect(() => articleStatusSchema.parse("archived")).toThrow(); + }); +}); + +describe("Article type", () => { + it("widens content to unknown", () => { + const _example: Article = { + id: "x", + title: "t", + slug: "s", + content: { whatever: true }, + status: "draft", + authorId: "u", + createdAt: new Date(), + updatedAt: new Date(), + }; + expect(_example).toBeDefined(); + }); +}); diff --git a/packages/blog/src/entities/article.ts b/packages/blog/src/entities/article.ts new file mode 100644 index 0000000..9f3354b --- /dev/null +++ b/packages/blog/src/entities/article.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const articleStatusSchema = z.enum(["draft", "published"]); + +export const articleSchema = z.object({ + id: z.string(), + title: z.string().min(1).max(255), + slug: z.string().min(1).max(255), + content: z.unknown(), + status: articleStatusSchema.default("draft"), + authorId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), +}); + +export type Article = z.infer; +export type ArticleStatus = z.infer; diff --git a/packages/blog/src/entities/errors.ts b/packages/blog/src/entities/errors.ts new file mode 100644 index 0000000..628de66 --- /dev/null +++ b/packages/blog/src/entities/errors.ts @@ -0,0 +1,11 @@ +export class ArticleNotFoundError extends Error { + constructor(message = "Article not found", options?: ErrorOptions) { + super(message, options); + } +} + +export class InputParseError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + } +}