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:
@@ -13,6 +13,7 @@ import { adminRouter } from "./modules/admin/router";
|
||||
import { aiRouter } from "./modules/ai/router";
|
||||
import { authRouter } from "./modules/auth/router";
|
||||
import { billingRouter } from "./modules/billing/router";
|
||||
import { diagramRouter } from "./modules/diagram/router";
|
||||
import { organizationRouter } from "./modules/organization/router";
|
||||
import { storageRouter } from "./modules/storage/router";
|
||||
import { onError } from "./utils/on-error";
|
||||
@@ -48,6 +49,7 @@ const appRouter = new Hono()
|
||||
.route("/ai", aiRouter)
|
||||
.route("/auth", authRouter)
|
||||
.route("/billing", billingRouter)
|
||||
.route("/diagrams", diagramRouter)
|
||||
.route("/organizations", organizationRouter)
|
||||
.route("/storage", storageRouter)
|
||||
.onError(onError);
|
||||
|
||||
73
packages/api/src/modules/diagram/router.ts
Normal file
73
packages/api/src/modules/diagram/router.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Hono } from "hono";
|
||||
import { and, desc, eq, isNull } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
import { diagram } from "@turbostarter/db/schema";
|
||||
import { db } from "@turbostarter/db/server";
|
||||
import { HttpStatusCode } from "@turbostarter/shared/constants";
|
||||
import { HttpException } from "@turbostarter/shared/utils";
|
||||
|
||||
import { enforceAuth, validate } from "../../middleware";
|
||||
|
||||
export 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()
|
||||
.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("/: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),
|
||||
isNull(diagram.deletedAt),
|
||||
),
|
||||
);
|
||||
|
||||
if (!d) {
|
||||
throw new HttpException(HttpStatusCode.NOT_FOUND, {
|
||||
code: "error.notFound",
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({ data: d });
|
||||
})
|
||||
.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 });
|
||||
},
|
||||
);
|
||||
112
packages/api/tests/diagram/diagram-db-schema.test.ts
Normal file
112
packages/api/tests/diagram/diagram-db-schema.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
134
packages/api/tests/diagram/diagram-schema.test.ts
Normal file
134
packages/api/tests/diagram/diagram-schema.test.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
314
packages/db/migrations/0000_simple_hobgoblin.sql
Normal file
314
packages/db/migrations/0000_simple_hobgoblin.sql
Normal file
@@ -0,0 +1,314 @@
|
||||
CREATE SCHEMA "pdf";
|
||||
--> statement-breakpoint
|
||||
CREATE TYPE "public"."credit_transaction_type" AS ENUM('signup', 'purchase', 'usage', 'admin_grant', 'admin_deduct', 'refund', 'promo', 'referral', 'expiry');--> statement-breakpoint
|
||||
CREATE TYPE "public"."status" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid');--> statement-breakpoint
|
||||
CREATE TYPE "public"."plan" AS ENUM('free', 'premium', 'enterprise');--> statement-breakpoint
|
||||
CREATE TYPE "public"."diagram_type" AS ENUM('bpmn', 'er', 'orgchart', 'architecture', 'sequence', 'flowchart');--> statement-breakpoint
|
||||
CREATE TYPE "chat"."role" AS ENUM('system', 'assistant', 'user');--> statement-breakpoint
|
||||
CREATE TYPE "pdf"."role" AS ENUM('user', 'assistant', 'system');--> statement-breakpoint
|
||||
CREATE TYPE "pdf"."processing_status" AS ENUM('pending', 'processing', 'ready', 'failed');--> statement-breakpoint
|
||||
CREATE TYPE "pdf"."unit_type" AS ENUM('prose', 'heading', 'list', 'table', 'code');--> statement-breakpoint
|
||||
CREATE TYPE "image"."aspect_ratio" AS ENUM('square', 'standard', 'landscape', 'portrait');--> statement-breakpoint
|
||||
CREATE TABLE "account" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"account_id" text NOT NULL,
|
||||
"provider_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"id_token" text,
|
||||
"access_token_expires_at" timestamp,
|
||||
"refresh_token_expires_at" timestamp,
|
||||
"scope" text,
|
||||
"password" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "invitation" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"role" text,
|
||||
"status" text DEFAULT 'pending' NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"inviter_id" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "member" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"role" text DEFAULT 'member' NOT NULL,
|
||||
"created_at" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "organization" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"slug" text NOT NULL,
|
||||
"logo" text,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"metadata" text,
|
||||
CONSTRAINT "organization_slug_unique" UNIQUE("slug")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "passkey" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text,
|
||||
"public_key" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"credential_id" text NOT NULL,
|
||||
"counter" integer NOT NULL,
|
||||
"device_type" text NOT NULL,
|
||||
"backed_up" boolean NOT NULL,
|
||||
"transports" text,
|
||||
"created_at" timestamp,
|
||||
"aaguid" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "session" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
"ip_address" text,
|
||||
"user_agent" text,
|
||||
"user_id" text NOT NULL,
|
||||
"impersonated_by" text,
|
||||
"active_organization_id" text,
|
||||
CONSTRAINT "session_token_unique" UNIQUE("token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "two_factor" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"secret" text NOT NULL,
|
||||
"backup_codes" text NOT NULL,
|
||||
"user_id" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"email_verified" boolean DEFAULT false NOT NULL,
|
||||
"image" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
"two_factor_enabled" boolean DEFAULT false,
|
||||
"is_anonymous" boolean DEFAULT false,
|
||||
"role" text,
|
||||
"banned" boolean DEFAULT false,
|
||||
"ban_reason" text,
|
||||
"ban_expires" timestamp,
|
||||
CONSTRAINT "user_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "verification" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"identifier" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "credit_transaction" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"customer_id" text NOT NULL,
|
||||
"amount" integer NOT NULL,
|
||||
"type" "credit_transaction_type" NOT NULL,
|
||||
"reason" text,
|
||||
"metadata" text,
|
||||
"balance_after" integer NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"created_by" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "customer" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"customer_id" text NOT NULL,
|
||||
"status" "status",
|
||||
"plan" "plan",
|
||||
"credits" integer DEFAULT 100 NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
CONSTRAINT "customer_userId_unique" UNIQUE("user_id"),
|
||||
CONSTRAINT "customer_customerId_unique" UNIQUE("customer_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "diagram" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"type" "diagram_type" NOT NULL,
|
||||
"graph_data" jsonb DEFAULT '{}'::jsonb,
|
||||
"user_id" text NOT NULL,
|
||||
"project_id" text,
|
||||
"last_ai_message" text,
|
||||
"deleted_at" timestamp,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "project" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"sort_order" integer DEFAULT 0,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "chat"."chat" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text,
|
||||
"user_id" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "chat"."message" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"chat_id" text NOT NULL,
|
||||
"role" "chat"."role" NOT NULL,
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "chat"."part" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"message_id" text NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"order" integer NOT NULL,
|
||||
"details" jsonb NOT NULL,
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "pdf"."chat" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text,
|
||||
"user_id" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "pdf"."citation_unit" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"document_id" text NOT NULL,
|
||||
"retrieval_chunk_id" text,
|
||||
"content" text NOT NULL,
|
||||
"page_number" integer NOT NULL,
|
||||
"paragraph_index" integer NOT NULL,
|
||||
"char_start" integer NOT NULL,
|
||||
"char_end" integer NOT NULL,
|
||||
"bbox_x" real,
|
||||
"bbox_y" real,
|
||||
"bbox_width" real,
|
||||
"bbox_height" real,
|
||||
"section_title" text,
|
||||
"unit_type" "pdf"."unit_type" DEFAULT 'prose',
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "pdf"."document" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"chat_id" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"path" text NOT NULL,
|
||||
"processing_status" "pdf"."processing_status" DEFAULT 'pending' NOT NULL,
|
||||
"processing_error" text,
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "pdf"."embedding" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"document_id" text NOT NULL,
|
||||
"content" text NOT NULL,
|
||||
"embedding" vector(1536) NOT NULL,
|
||||
"page_number" integer,
|
||||
"char_start" integer,
|
||||
"char_end" integer,
|
||||
"section_title" text,
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "pdf"."message" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"chat_id" text NOT NULL,
|
||||
"content" text NOT NULL,
|
||||
"role" "pdf"."role" NOT NULL,
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "pdf"."retrieval_chunk" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"document_id" text NOT NULL,
|
||||
"content" text NOT NULL,
|
||||
"embedding" vector(1536),
|
||||
"page_start" integer NOT NULL,
|
||||
"page_end" integer NOT NULL,
|
||||
"section_hierarchy" text[],
|
||||
"chunk_type" text DEFAULT 'prose',
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "image"."generation" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"prompt" text NOT NULL,
|
||||
"model" text NOT NULL,
|
||||
"aspect_ratio" "image"."aspect_ratio" DEFAULT 'square' NOT NULL,
|
||||
"count" integer DEFAULT 1 NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"completed_at" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "image"."image" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"generation_id" text NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD CONSTRAINT "passkey_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "credit_transaction" ADD CONSTRAINT "credit_transaction_customer_id_customer_id_fk" FOREIGN KEY ("customer_id") REFERENCES "public"."customer"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "customer" ADD CONSTRAINT "customer_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "diagram" ADD CONSTRAINT "diagram_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "project" ADD CONSTRAINT "project_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "chat"."chat" ADD CONSTRAINT "chat_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "chat"."message" ADD CONSTRAINT "message_chat_id_chat_id_fk" FOREIGN KEY ("chat_id") REFERENCES "chat"."chat"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "chat"."part" ADD CONSTRAINT "part_message_id_message_id_fk" FOREIGN KEY ("message_id") REFERENCES "chat"."message"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "pdf"."chat" ADD CONSTRAINT "chat_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "pdf"."citation_unit" ADD CONSTRAINT "citation_unit_document_id_document_id_fk" FOREIGN KEY ("document_id") REFERENCES "pdf"."document"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "pdf"."citation_unit" ADD CONSTRAINT "citation_unit_retrieval_chunk_id_retrieval_chunk_id_fk" FOREIGN KEY ("retrieval_chunk_id") REFERENCES "pdf"."retrieval_chunk"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "pdf"."document" ADD CONSTRAINT "document_chat_id_chat_id_fk" FOREIGN KEY ("chat_id") REFERENCES "pdf"."chat"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "pdf"."embedding" ADD CONSTRAINT "embedding_document_id_document_id_fk" FOREIGN KEY ("document_id") REFERENCES "pdf"."document"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "pdf"."message" ADD CONSTRAINT "message_chat_id_chat_id_fk" FOREIGN KEY ("chat_id") REFERENCES "pdf"."chat"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "pdf"."retrieval_chunk" ADD CONSTRAINT "retrieval_chunk_document_id_document_id_fk" FOREIGN KEY ("document_id") REFERENCES "pdf"."document"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "image"."generation" ADD CONSTRAINT "generation_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "image"."image" ADD CONSTRAINT "image_generation_id_generation_id_fk" FOREIGN KEY ("generation_id") REFERENCES "image"."generation"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "invitation_organizationId_idx" ON "invitation" USING btree ("organization_id");--> statement-breakpoint
|
||||
CREATE INDEX "invitation_email_idx" ON "invitation" USING btree ("email");--> statement-breakpoint
|
||||
CREATE INDEX "member_organizationId_idx" ON "member" USING btree ("organization_id");--> statement-breakpoint
|
||||
CREATE INDEX "member_userId_idx" ON "member" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "passkey_userId_idx" ON "passkey" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "passkey_credentialID_idx" ON "passkey" USING btree ("credential_id");--> statement-breakpoint
|
||||
CREATE INDEX "session_userId_idx" ON "session" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "twoFactor_secret_idx" ON "two_factor" USING btree ("secret");--> statement-breakpoint
|
||||
CREATE INDEX "twoFactor_userId_idx" ON "two_factor" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier");--> statement-breakpoint
|
||||
CREATE INDEX "idx_cu_document" ON "pdf"."citation_unit" USING btree ("document_id");--> statement-breakpoint
|
||||
CREATE INDEX "idx_cu_retrieval" ON "pdf"."citation_unit" USING btree ("retrieval_chunk_id");--> statement-breakpoint
|
||||
CREATE INDEX "idx_cu_page" ON "pdf"."citation_unit" USING btree ("document_id","page_number");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "idx_cu_unique" ON "pdf"."citation_unit" USING btree ("document_id","page_number","paragraph_index");--> statement-breakpoint
|
||||
CREATE INDEX "pdf_embeddingIndex" ON "pdf"."embedding" USING hnsw ("embedding" vector_cosine_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_rc_document" ON "pdf"."retrieval_chunk" USING btree ("document_id");--> statement-breakpoint
|
||||
CREATE INDEX "idx_rc_embedding" ON "pdf"."retrieval_chunk" USING hnsw ("embedding" vector_cosine_ops);
|
||||
2263
packages/db/migrations/meta/0000_snapshot.json
Normal file
2263
packages/db/migrations/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
13
packages/db/migrations/meta/_journal.json
Normal file
13
packages/db/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1771801819664,
|
||||
"tag": "0000_simple_hobgoblin",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
71
packages/db/src/schema/diagram.ts
Normal file
71
packages/db/src/schema/diagram.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
integer,
|
||||
jsonb,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
import { generateId } from "@turbostarter/shared/utils";
|
||||
|
||||
import {
|
||||
createInsertSchema,
|
||||
createSelectSchema,
|
||||
createUpdateSchema,
|
||||
} from "../lib/zod";
|
||||
|
||||
import { user } from "./auth";
|
||||
|
||||
import type * as z from "zod";
|
||||
|
||||
export const diagramTypeEnum = pgEnum("diagram_type", [
|
||||
"bpmn",
|
||||
"er",
|
||||
"orgchart",
|
||||
"architecture",
|
||||
"sequence",
|
||||
"flowchart",
|
||||
]);
|
||||
|
||||
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 const diagram = pgTable("diagram", {
|
||||
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||
title: text().notNull(),
|
||||
type: diagramTypeEnum().notNull(),
|
||||
graphData: jsonb().$type<object>().default({}),
|
||||
userId: text()
|
||||
.references(() => user.id, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
projectId: text(),
|
||||
lastAiMessage: text(),
|
||||
deletedAt: timestamp(),
|
||||
createdAt: timestamp().defaultNow(),
|
||||
updatedAt: timestamp().$onUpdate(() => new Date()),
|
||||
});
|
||||
|
||||
export const insertDiagramSchema = createInsertSchema(diagram);
|
||||
export const selectDiagramSchema = createSelectSchema(diagram);
|
||||
export const updateDiagramSchema = createUpdateSchema(diagram);
|
||||
|
||||
export type InsertDiagram = z.infer<typeof insertDiagramSchema>;
|
||||
export type SelectDiagram = z.infer<typeof selectDiagramSchema>;
|
||||
export type UpdateDiagram = z.infer<typeof updateDiagramSchema>;
|
||||
|
||||
export const insertProjectSchema = createInsertSchema(project);
|
||||
export const selectProjectSchema = createSelectSchema(project);
|
||||
export const updateProjectSchema = createUpdateSchema(project);
|
||||
|
||||
export type InsertProject = z.infer<typeof insertProjectSchema>;
|
||||
export type SelectProject = z.infer<typeof selectProjectSchema>;
|
||||
export type UpdateProject = z.infer<typeof updateProjectSchema>;
|
||||
@@ -2,6 +2,7 @@ import * as auth from "./auth";
|
||||
import * as chat from "./chat";
|
||||
import * as creditTransactions from "./credit-transaction";
|
||||
import * as customers from "./customer";
|
||||
import * as diagramModule from "./diagram";
|
||||
import * as image from "./image";
|
||||
import * as pdf from "./pdf";
|
||||
|
||||
@@ -27,6 +28,7 @@ export const schema = {
|
||||
...auth,
|
||||
...creditTransactions,
|
||||
...customers,
|
||||
...diagramModule,
|
||||
...prefix(chat, "chat"),
|
||||
...prefix(pdf, "pdf"),
|
||||
...prefix(image, "image"),
|
||||
@@ -36,6 +38,7 @@ export const schema = {
|
||||
export * from "./auth";
|
||||
export * from "./credit-transaction";
|
||||
export * from "./customer";
|
||||
export * from "./diagram";
|
||||
|
||||
// pgSchema-based modules (need explicit exports for drizzle-kit)
|
||||
export * from "./chat";
|
||||
|
||||
@@ -149,6 +149,10 @@ import {
|
||||
ScrollText,
|
||||
Mic,
|
||||
MicOff,
|
||||
Workflow,
|
||||
Server,
|
||||
ArrowRightLeft,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Icons as GlobalIcons } from "@turbostarter/ui/assets";
|
||||
@@ -373,6 +377,10 @@ export const Icons = {
|
||||
ScrollText,
|
||||
Mic,
|
||||
MicOff,
|
||||
Workflow,
|
||||
Server,
|
||||
ArrowRightLeft,
|
||||
GitBranch,
|
||||
MinusIcon: Minus,
|
||||
PlusIcon: Plus,
|
||||
// AI provider icons
|
||||
|
||||
Reference in New Issue
Block a user