feat: implement Story 1.1 — create and view diagrams

Add diagram/project DB schema, CRUD API, dashboard pages with grid/card/
empty state, create dialog with type selector, editor placeholder, and
26 schema validation tests. Includes code review fixes: soft-delete
filter on GET /:id, error handling, keyboard accessibility, type-safe
API response types, and error states on pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-22 23:54:50 +00:00
parent da3368fbdb
commit 392da385f4
20 changed files with 3785 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
import { describe, it, expect } from "vitest";
import {
insertDiagramSchema,
selectDiagramSchema,
insertProjectSchema,
selectProjectSchema,
} from "@turbostarter/db/schema";
describe("insertDiagramSchema", () => {
it("should accept valid diagram insert data", () => {
const result = insertDiagramSchema.safeParse({
title: "Test Diagram",
type: "flowchart",
userId: "user-123",
});
expect(result.success).toBe(true);
});
it("should reject missing required fields", () => {
const result = insertDiagramSchema.safeParse({});
expect(result.success).toBe(false);
});
it("should reject invalid diagram type", () => {
const result = insertDiagramSchema.safeParse({
title: "Test",
type: "invalid-type",
userId: "user-123",
});
expect(result.success).toBe(false);
});
it("should accept all valid diagram types", () => {
const types = ["bpmn", "er", "orgchart", "architecture", "sequence", "flowchart"];
for (const type of types) {
const result = insertDiagramSchema.safeParse({
title: "Test",
type,
userId: "user-123",
});
expect(result.success).toBe(true);
}
});
it("should accept optional fields", () => {
const result = insertDiagramSchema.safeParse({
title: "Test",
type: "bpmn",
userId: "user-123",
projectId: "proj-123",
lastAiMessage: "Hello",
graphData: { nodes: [] },
});
expect(result.success).toBe(true);
});
});
describe("selectDiagramSchema", () => {
it("should accept a complete diagram record", () => {
const result = selectDiagramSchema.safeParse({
id: "diag-123",
title: "Test Diagram",
type: "er",
graphData: {},
userId: "user-123",
projectId: null,
lastAiMessage: null,
deletedAt: null,
createdAt: new Date(),
updatedAt: new Date(),
});
expect(result.success).toBe(true);
});
});
describe("insertProjectSchema", () => {
it("should accept valid project insert data with field assertions", () => {
const result = insertProjectSchema.safeParse({
name: "My Project",
userId: "user-123",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.name).toBe("My Project");
expect(result.data.userId).toBe("user-123");
}
});
it("should accept optional sortOrder", () => {
const result = insertProjectSchema.safeParse({
name: "My Project",
userId: "user-123",
sortOrder: 5,
});
expect(result.success).toBe(true);
});
});
describe("selectProjectSchema", () => {
it("should accept a complete project record", () => {
const result = selectProjectSchema.safeParse({
id: "proj-123",
name: "My Project",
userId: "user-123",
sortOrder: 0,
createdAt: new Date(),
updatedAt: new Date(),
});
expect(result.success).toBe(true);
});
});

View File

@@ -0,0 +1,134 @@
import { describe, it, expect } from "vitest";
import { createDiagramSchema } from "../../src/modules/diagram/router";
describe("createDiagramSchema", () => {
describe("title field", () => {
it("should accept a valid title", () => {
const result = createDiagramSchema.safeParse({
title: "My Diagram",
type: "flowchart",
});
expect(result.success).toBe(true);
});
it("should reject empty title", () => {
const result = createDiagramSchema.safeParse({
title: "",
type: "flowchart",
});
expect(result.success).toBe(false);
});
it("should reject title over 255 characters", () => {
const result = createDiagramSchema.safeParse({
title: "a".repeat(256),
type: "flowchart",
});
expect(result.success).toBe(false);
});
it("should accept title at max length (255)", () => {
const result = createDiagramSchema.safeParse({
title: "a".repeat(255),
type: "flowchart",
});
expect(result.success).toBe(true);
});
it("should reject missing title", () => {
const result = createDiagramSchema.safeParse({
type: "flowchart",
});
expect(result.success).toBe(false);
});
});
describe("type field", () => {
const validTypes = [
"bpmn",
"er",
"orgchart",
"architecture",
"sequence",
"flowchart",
] as const;
validTypes.forEach((type) => {
it(`should accept type '${type}'`, () => {
const result = createDiagramSchema.safeParse({
title: "Test",
type,
});
expect(result.success).toBe(true);
});
});
it("should reject invalid type", () => {
const result = createDiagramSchema.safeParse({
title: "Test",
type: "invalid",
});
expect(result.success).toBe(false);
});
it("should reject missing type", () => {
const result = createDiagramSchema.safeParse({
title: "Test",
});
expect(result.success).toBe(false);
});
});
describe("projectId field", () => {
it("should accept optional projectId", () => {
const result = createDiagramSchema.safeParse({
title: "Test",
type: "bpmn",
projectId: "proj-123",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.projectId).toBe("proj-123");
}
});
it("should accept missing projectId", () => {
const result = createDiagramSchema.safeParse({
title: "Test",
type: "bpmn",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.projectId).toBeUndefined();
}
});
});
describe("complete valid input", () => {
it("should parse valid input correctly", () => {
const input = {
title: "System Architecture",
type: "architecture" as const,
projectId: "proj-abc",
};
const result = createDiagramSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(input);
}
});
it("should strip unknown fields", () => {
const result = createDiagramSchema.safeParse({
title: "Test",
type: "flowchart",
unknownField: "should be stripped",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).not.toHaveProperty("unknownField");
}
});
});
});