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>
16 KiB
Story 1.1: Create and View Diagrams
Status: done
Story
As a user, I want to create new diagrams and see them listed in my dashboard, so that I can start building system designs and find them later.
Acceptance Criteria
-
Given I am authenticated (via TurboStarter auth), When I navigate to the dashboard, Then I see a diagram list page with any existing diagrams displayed as cards showing title, diagram type icon, and last edited timestamp, And I see a "New Diagram" button in the dashboard header.
-
Given I click "New Diagram", When I enter a title and select a diagram type (BPMN, E-R, Org Chart, Architecture, Sequence, Flowchart) in the creation modal, Then a new diagram record is created in the database with my userId as owner, And I am navigated to the diagram editor route (
/dashboard/diagram/[id]). -
Given I have created multiple diagrams, When I view my dashboard, Then all my diagrams are listed sorted by last modified date descending, And each card shows the diagram title, type badge, and relative timestamp ("2 hours ago").
-
Given I have no diagrams yet, When I view the dashboard, Then I see an empty state with a call-to-action: "Create your first diagram" button and a brief description of what domaingraph does.
Tasks / Subtasks
- Task 1: Create diagram database schema (AC: #1, #2)
- 1.1: Create
packages/db/src/schema/diagram.tswithdiagramtable anddiagramTypeEnum - 1.2: Create
projecttable in same file (needed for nullable FK, full CRUD in Story 1.2) - 1.3: Export Zod schemas and inferred types
- 1.4: Register in
packages/db/src/schema/index.ts - 1.5: Run
pnpm --filter @turbostarter/db generateandpnpm --filter @turbostarter/db migrate
- 1.1: Create
- Task 2: Create diagram API module (AC: #1, #2, #3)
- 2.1: Create
packages/api/src/modules/diagram/router.tswith CRUD routes - 2.2: Create Zod validation schemas for create/update inputs
- 2.3: Register route in
packages/api/src/index.ts
- 2.1: Create
- Task 3: Create dashboard diagrams page (AC: #1, #3, #4)
- 3.1: Create
apps/web/src/app/[locale]/dashboard/(user)/diagrams/page.tsx - 3.2: Create
apps/web/src/modules/diagram/components/DiagramCard.tsx - 3.3: Create
apps/web/src/modules/diagram/components/DiagramGrid.tsx - 3.4: Create empty state component
- 3.1: Create
- Task 4: Create "New Diagram" modal (AC: #2)
- 4.1: Create
apps/web/src/modules/diagram/components/CreateDiagramDialog.tsx - 4.2: Create diagram type selector cards with icons
- 4.3: Wire API call with React Query mutation + navigation
- 4.1: Create
- Task 5: Create diagram editor placeholder page (AC: #2)
- 5.1: Create
apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsxas placeholder
- 5.1: Create
- Task 6: Tests
- 6.1: API router tests for diagram CRUD
- 6.2: Schema validation tests
Dev Notes
Database Schema — packages/db/src/schema/diagram.ts
CRITICAL PATTERNS (from existing codebase):
// Follow chat.ts pattern EXACTLY — DO NOT use pgSchema for diagram (use pgTable)
// pgSchema is ONLY for AI feature domains (chat, pdf, image)
import { pgTable, pgEnum, text, timestamp, jsonb, integer } from "drizzle-orm/pg-core";
import { generateId } from "@turbostarter/shared/utils";
import { createInsertSchema, createSelectSchema } from "../utils/drizzle-zod";
import { user } from "./auth";
// Use pgEnum for DB-backed enums
export const diagramTypeEnum = pgEnum("diagram_type", [
"bpmn", "er", "orgchart", "architecture", "sequence", "flowchart"
]);
export const diagram = pgTable("diagram", {
id: text().primaryKey().notNull().$defaultFn(generateId), // NEVER uuid()
title: text().notNull(),
type: diagramTypeEnum().notNull(),
graphData: jsonb().$type<object>().default({}), // Unified graph model JSON
userId: text().references(() => user.id, { onDelete: "cascade" }).notNull(),
projectId: text(), // Nullable FK — project table in same file, wired in Story 1.2
lastAiMessage: text(), // Cached last AI chat message for preview (Story 1.4)
deletedAt: timestamp(), // Soft delete (Story 1.3)
createdAt: timestamp().defaultNow(),
updatedAt: timestamp().$onUpdate(() => new Date()),
});
// Also define project table stub (full CRUD in Story 1.2)
export const project = pgTable("project", {
id: text().primaryKey().notNull().$defaultFn(generateId),
name: text().notNull(),
userId: text().references(() => user.id, { onDelete: "cascade" }).notNull(),
sortOrder: integer().default(0),
createdAt: timestamp().defaultNow(),
updatedAt: timestamp().$onUpdate(() => new Date()),
});
// Export Zod schemas + inferred types (REQUIRED pattern)
export const selectDiagramSchema = createSelectSchema(diagram);
export const insertDiagramSchema = createInsertSchema(diagram);
export type SelectDiagram = typeof diagram.$inferSelect;
export type InsertDiagram = typeof diagram.$inferInsert;
// Same for project...
Schema registration in packages/db/src/schema/index.ts:
- Diagram uses
pgTable(public schema) → add with spread:...diagram(NOTprefix()) prefix()is ONLY forpgSchema()modules (chat, pdf, image)- Add:
import * as diagramModule from "./diagram";then...diagramModulein schema object andexport * from "./diagram";
API Module — packages/api/src/modules/diagram/router.ts
Follow billing router pattern:
import { Hono } from "hono";
import { eq, desc, and, isNull } from "drizzle-orm";
import { z } from "zod";
import { diagram } from "@turbostarter/db/schema";
import { db } from "@turbostarter/db/server";
import { enforceAuth, validate } from "../../middleware";
import { generateId } from "@turbostarter/shared/utils";
import { HttpStatusCode, HttpException } from "@turbostarter/shared/utils";
const createDiagramSchema = z.object({
title: z.string().min(1).max(255),
type: z.enum(["bpmn", "er", "orgchart", "architecture", "sequence", "flowchart"]),
projectId: z.string().optional(),
});
export const diagramRouter = new Hono()
// List user's diagrams — sorted by updatedAt desc, exclude soft-deleted
.get("/", enforceAuth, async (c) => {
const diagrams = await db
.select()
.from(diagram)
.where(and(
eq(diagram.userId, c.var.user.id),
isNull(diagram.deletedAt)
))
.orderBy(desc(diagram.updatedAt));
return c.json({ data: diagrams });
})
// Get single diagram
.get("/:id", enforceAuth, async (c) => {
const [d] = await db.select().from(diagram)
.where(and(eq(diagram.id, c.req.param("id")), eq(diagram.userId, c.var.user.id)));
if (!d) throw new HttpException(HttpStatusCode.NOT_FOUND, { code: "error.notFound" });
return c.json({ data: d });
})
// Create diagram
.post("/", enforceAuth, validate("json", createDiagramSchema), async (c) => {
const input = c.req.valid("json");
const [created] = await db.insert(diagram).values({
...input,
userId: c.var.user.id,
}).returning();
return c.json({ data: created });
});
Register in packages/api/src/index.ts:
import { diagramRouter } from "./modules/diagram/router";
// In appRouter chain:
.route("/diagrams", diagramRouter)
Dashboard Page — apps/web/src/app/[locale]/dashboard/(user)/diagrams/page.tsx
Architecture specifies: app/(dashboard)/diagram/[id]/ but TurboStarter uses app/[locale]/dashboard/(user)/. Follow TurboStarter routing.
- Page route:
apps/web/src/app/[locale]/dashboard/(user)/diagrams/page.tsx - Editor placeholder:
apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx - Feature modules:
apps/web/src/modules/diagram/(NOT co-located in route dirs)
Data fetching: Use @tanstack/react-query for client-side data fetching via Hono RPC client:
- Import API client from
~/lib/api/client useQueryfor listing,useMutationfor create
UI Components to use (from @turbostarter/ui-web/):
Card,CardContent,CardHeader,CardTitle— for diagram cardsDialog,DialogContent,DialogHeader,DialogTitle,DialogTrigger— for create modalButton— for "New Diagram" CTAIcons— for diagram type iconsBadge— for diagram type badge on cardsInput— for title field
Empty state: When no diagrams, show illustration + "Create your first diagram" CTA button.
Diagram type icons mapping:
- BPMN →
Icons.Workflow(or similar) - E-R →
Icons.Database - Org Chart →
Icons.Users - Architecture →
Icons.Server - Sequence →
Icons.ArrowRightLeft - Flowchart →
Icons.GitBranch
Diagram type color accents (from UX spec):
- BPMN: blue
- E-R: violet
- Org Chart: green
- Architecture: neutral
- Sequence: amber
- Flowchart: rose
Navigation
- After creating a diagram, navigate to
/dashboard/diagram/[id] - Use
pathsConfigfrom~/config/paths— add diagram paths there - Use Next.js
useRouter().push()for client navigation
Project Structure Notes
packages/db/src/schema/diagram.ts— new file (pgTable, NOT pgSchema)packages/api/src/modules/diagram/router.ts— new filepackages/api/src/index.ts— modify to add.route("/diagrams", diagramRouter)packages/db/src/schema/index.ts— modify to add diagram exportsapps/web/src/app/[locale]/dashboard/(user)/diagrams/page.tsx— new pageapps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx— new placeholder pageapps/web/src/modules/diagram/components/DiagramCard.tsx— new componentapps/web/src/modules/diagram/components/DiagramGrid.tsx— new componentapps/web/src/modules/diagram/components/CreateDiagramDialog.tsx— new componentapps/web/src/config/paths.ts— modify to add diagram paths
Anti-Patterns to Avoid
- NEVER use
uuid()column type — usetext().primaryKey().$defaultFn(generateId) - NEVER use
pgSchema("diagram")— diagrams are public tables viapgTable() - NEVER put business logic in the API router — keep handlers thin
- NEVER use
require()— ESM only - NEVER use raw HTTP status numbers — use
HttpStatusCodeenum - NEVER inline
.parse()— usevalidate()middleware - NEVER co-locate feature code in route directories — use
~/modules/diagram/ - DO NOT use
prefix()when registering diagram in schema/index.ts (only pgSchema modules)
References
- [Source: packages/db/src/schema/chat.ts] — DB schema pattern with pgSchema, generateId, timestamps, FK, Zod schemas
- [Source: packages/db/src/schema/customer.ts] — DB schema pattern with pgTable (public schema)
- [Source: packages/api/src/modules/billing/router.ts] — API router pattern with enforceAuth, validate, handler delegation
- [Source: packages/api/src/index.ts] — Route registration pattern
- [Source: packages/db/src/schema/index.ts] — Schema export pattern with prefix() for pgSchema modules
- [Source: apps/web/src/app/[locale]/dashboard/(user)/page.tsx] — Dashboard page pattern with Card components
- [Source: _bmad-output/planning-artifacts/architecture.md] — Architecture Decision 1 (graph data model), naming patterns, API module structure
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md] — Studio layout, diagram type accents, empty states, responsive breakpoints
- [Source: _bmad-output/planning-artifacts/epics.md] — Story 1.1 acceptance criteria, technical notes
- [Source: _bmad-output/project-context.md] — All critical implementation rules
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (team-based: backend + frontend agents in parallel)
Debug Log References
- Migration generated successfully (
0000_simple_hobgoblin.sql) butdb:migrateskipped (no DATABASE_URL in local env — requirespnpm services:start) date-fnsnot available in project — used lightweight inlinetimeAgo()helper instead- Drizzle insert schemas with
coerce: truemake most fields optional — adjusted test expectations accordingly
Completion Notes List
- Task 1: Created
diagramandprojecttables with pgTable pattern, diagramTypeEnum pgEnum, Zod schemas + types. Registered in schema index with spread (no prefix). - Task 2: Created diagram API router with GET / (list), GET /:id (single), POST / (create). Follows billing router pattern with enforceAuth + validate middleware.
- Task 3: Created diagrams page with DiagramGrid (responsive grid), DiagramCard (type icon, badge, relative time), EmptyDiagrams (dashed border CTA).
- Task 4: Created CreateDiagramDialog with title input + 6-type visual selector grid, React Query mutation, auto-redirect to editor on success.
- Task 5: Created diagram editor placeholder page that fetches diagram by ID and shows placeholder message for Epic 2.
- Task 6: Created 26 tests across 2 test files — API schema validation (17 tests) and DB schema validation (9 tests). All 92 tests pass (including existing).
File List
packages/db/src/schema/diagram.ts— NEW: diagram + project tables, enums, Zod schemas, typespackages/db/src/schema/index.ts— MODIFIED: added diagramModule import, spread, and re-exportpackages/db/migrations/0000_simple_hobgoblin.sql— NEW: generated migrationpackages/api/src/modules/diagram/router.ts— NEW: diagram CRUD API routerpackages/api/src/index.ts— MODIFIED: added diagramRouter import and route registrationapps/web/src/config/paths.ts— MODIFIED: added diagrams + diagram(id) to dashboard.userpackages/ui/web/src/components/icons.tsx— MODIFIED: added Workflow, Server, ArrowRightLeft, GitBranch iconsapps/web/src/app/[locale]/dashboard/(user)/diagrams/page.tsx— NEW: diagrams list pageapps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx— NEW: editor placeholder pageapps/web/src/modules/diagram/components/DiagramCard.tsx— NEW: diagram card componentapps/web/src/modules/diagram/components/DiagramGrid.tsx— NEW: diagram grid with headerapps/web/src/modules/diagram/components/EmptyDiagrams.tsx— NEW: empty state componentapps/web/src/modules/diagram/components/CreateDiagramDialog.tsx— NEW: create diagram modalpackages/api/tests/diagram/diagram-schema.test.ts— NEW: API schema validation tests (17 tests)packages/api/tests/diagram/diagram-db-schema.test.ts— NEW: DB schema validation tests (10 tests)
Senior Developer Review (AI)
Review Model
Claude Opus 4.6
Findings Summary
10 issues found (2 HIGH, 5 MEDIUM, 3 LOW). All auto-fixed.
Issues Fixed
- HIGH — Soft-delete bypass in GET /:id: Added
isNull(diagram.deletedAt)to single-diagram fetch (router.ts) - HIGH — No error handling on create mutation: Added
onErrorwithtoast.error()to CreateDiagramDialog - MEDIUM — DiagramCard not keyboard accessible: Added
role="button",tabIndex={0},onKeyDownhandler for Enter/Space - MEDIUM — Duplicate test in db-schema tests: Merged "should accept valid project insert data" and "should accept minimal project data" into single test with assertions
- MEDIUM — No error state in diagrams page: Added
isErrorhandling with AlertTriangle icon and error message - MEDIUM — No error state in editor placeholder: Added
isErrorhandling for failed diagram fetch - MEDIUM — Type mismatch: Date vs string in API response: Created
DiagramResponsetype alias with string dates for JSON-serialized API responses - LOW — timeAgo edge cases: Added guard for negative seconds (future dates) and year display for 12+ months
- LOW — data.data possibly undefined: Added null check before
router.pushin onSuccess callback
Test Results After Review
- 92 tests pass (7 test files)
- TypeScript compilation: 0 errors
Change Log
- 2026-02-22: Implemented Story 1.1 — Create and View Diagrams. Added diagram/project DB schema, diagram CRUD API, dashboard diagrams page with grid/card/empty state, create diagram modal with type selector, editor placeholder page, and 27 schema validation tests.
- 2026-02-22: Code review fixes — soft-delete bypass in GET /:id, error handling on create mutation, keyboard accessibility on DiagramCard, error states in pages, type-safe API response types, timeAgo edge cases, removed duplicate test.