diff --git a/apps/api/src/routes/analytics.ts b/apps/api/src/routes/analytics.ts index e0c5b66..3a1f93b 100644 --- a/apps/api/src/routes/analytics.ts +++ b/apps/api/src/routes/analytics.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { db, tasks, habits, habitCompletions, projects } from "@project-e/db"; -import { and, eq, gte, inArray, isNull, or } from "drizzle-orm"; +import { and, eq, gte, inArray, isNull, isNotNull, or } from "drizzle-orm"; import { requireAuth, resolveActiveDomain, requireWorkspaceAccess, AuthError } from "../middleware/auth"; export const analyticsRoutes = new Hono(); @@ -30,7 +30,7 @@ analyticsRoutes.get("/productivity", async (c) => { isNull(tasks.deletedAt), )); - const completedTasks = allTasks.filter(t => t.status === "done"); + const completedTasks = allTasks.filter(t => t.completedAt !== null); const taskCompletionRate = allTasks.length > 0 ? Math.round((completedTasks.length / allTasks.length) * 100) : 0; return c.json({ @@ -163,9 +163,9 @@ analyticsRoutes.get("/projects", async (c) => { const projectIds = allProjects.map((p) => p.id); - // Count tasks per project (any status, including non-done) for the domain + // Count tasks per project for the domain const taskRows = projectIds.length > 0 - ? await db.select({ projectId: tasks.projectId, status: tasks.status }) + ? await db.select({ projectId: tasks.projectId, completedAt: tasks.completedAt }) .from(tasks) .where(and( isNull(tasks.deletedAt), @@ -178,7 +178,7 @@ analyticsRoutes.get("/projects", async (c) => { if (!t.projectId) continue; const entry = counts.get(t.projectId) ?? { totalTasks: 0, completedTasks: 0 }; entry.totalTasks += 1; - if (t.status === "done") entry.completedTasks += 1; + if (t.completedAt) entry.completedTasks += 1; counts.set(t.projectId, entry); } @@ -261,7 +261,7 @@ analyticsRoutes.get("/cycle", async (c) => { await requireWorkspaceAccess(c, domainId); const startDate = new Date(); startDate.setDate(startDate.getDate() - range); - const doneTasks = await db.select().from(tasks).where(and(eq(tasks.domainId, domainId), eq(tasks.status, "done"), gte(tasks.completedAt, startDate), isNull(tasks.deletedAt))); + const doneTasks = await db.select().from(tasks).where(and(eq(tasks.domainId, domainId), isNotNull(tasks.completedAt), gte(tasks.completedAt, startDate), isNull(tasks.deletedAt))); const durations: number[] = []; for (const t of doneTasks) if (t.completedAt) durations.push((t.completedAt.getTime() - t.createdAt.getTime()) / (1000*60*60*24)); durations.sort((a,b)=>a-b); @@ -319,7 +319,7 @@ analyticsRoutes.get("/daily", async (c) => { for (const t of domainTasks) { const createdKey = localDateKey(t.createdAt); createdByDay.set(createdKey, (createdByDay.get(createdKey) || 0) + 1); - if (t.status === "done" && t.completedAt) { + if (t.completedAt) { const completedKey = localDateKey(t.completedAt); completedByDay.set(completedKey, (completedByDay.get(completedKey) || 0) + 1); } diff --git a/apps/api/src/routes/graph.ts b/apps/api/src/routes/graph.ts index 60ab574..ceba2b5 100644 --- a/apps/api/src/routes/graph.ts +++ b/apps/api/src/routes/graph.ts @@ -1,5 +1,5 @@ import { Hono } from "hono"; -import { db, domains, notes, noteLinks, noteEntityLinks, tasks, taskDependencies, habits, projects, sections, tags as tagsTable } from "@project-e/db"; +import { db, domains, notes, tasks, habits, projects, sections, tags as tagsTable, links } from "@project-e/db"; import { and, eq, inArray, isNull } from "drizzle-orm"; import { requireAuth, requireWorkspaceAccess, AuthError } from "../middleware/auth"; import { recordActivity } from "../middleware/activity"; @@ -57,18 +57,12 @@ async function getGraphData(domainId: string): Promise<{ nodes: GraphNode[]; edg for (const s of sectionRows) addNode(s.id, s.name, 'section'); for (const t of tagRows) addNode(t.id, t.name, 'tag'); - const noteIds = noteRows.map(n => n.id); - if (noteIds.length > 0) { - const linkRows = await db.select().from(noteLinks).where(inArray(noteLinks.sourceNoteId, noteIds)); - for (const l of linkRows) addEdge(l.sourceNoteId, l.targetNoteId, 'note_link'); - const entityLinkRows = await db.select().from(noteEntityLinks).where(inArray(noteEntityLinks.noteId, noteIds)); - for (const l of entityLinkRows) addEdge(l.noteId, l.entityId, 'note_' + l.entityType); - } - - const taskIds = taskRows.map(t => t.id); - if (taskIds.length > 0) { - const depRows = await db.select().from(taskDependencies).where(inArray(taskDependencies.taskId, taskIds)); - for (const d of depRows) addEdge(d.taskId, d.dependsOnTaskId, 'depends_on'); + // Read links from the canonical links table + const allIds = [...noteRows.map(n => n.id), ...taskRows.map(t => t.id)]; + if (allIds.length > 0) { + const linkRows = await db.select().from(links) + .where(inArray(links.sourceId, allIds)); + for (const l of linkRows) addEdge(l.sourceId, l.targetId, l.linkType); } for (const t of taskRows) { if (t.projectId) addEdge(t.id, t.projectId, 'task_project'); addEdge(t.id, domainId, 'task_domain'); } @@ -80,20 +74,6 @@ async function getGraphData(domainId: string): Promise<{ nodes: GraphNode[]; edg return { nodes, edges }; } -// Resolve the owning domain for a graph edge source. `type` may be an edge type -// (note_link / note_entity / task_dependency) or a source entity type (note / task). -async function resolveEdgeWorkspaceId(sourceId: string, type: string): Promise { - if (type === "note_link" || type === "note_entity" || type === "note") { - const [row] = await db.select({ domainId: notes.domainId }).from(notes).where(eq(notes.id, sourceId)).limit(1); - return row?.domainId ?? null; - } - if (type === "task_dependency" || type === "task") { - const [row] = await db.select({ domainId: tasks.domainId }).from(tasks).where(eq(tasks.id, sourceId)).limit(1); - return row?.domainId ?? null; - } - return null; -} - // GET /api/graph/nodes — All nodes graphRoutes.get("/nodes", async (c) => { try { @@ -136,41 +116,34 @@ graphRoutes.get("/edges", async (c) => { } }); -// POST /api/graph/edges — Create a relationship (note link) +// POST /api/graph/edges — Create a relationship via the links table graphRoutes.post("/edges", async (c) => { try { const user = await requireAuth(c); const body = await c.req.json(); - const { sourceId, targetId, type } = z.object({ + const { sourceId, targetId, type, sourceType, targetType } = z.object({ sourceId: z.string().uuid(), targetId: z.string().uuid(), - type: z.string().default("note_link"), + type: z.string().default("relates"), + sourceType: z.string().default("note"), + targetType: z.string().default("note"), }).parse(body); - // Verify ownership before mutating anything. Both endpoints of the edge - // must belong to the caller's domain. - const workspaceId = await resolveEdgeWorkspaceId(sourceId, type); + const workspaceId = await resolveEdgeWorkspaceId(sourceId, sourceType); if (workspaceId) { await requireWorkspaceAccess(c, workspaceId); } - const targetType = type === "note_link" ? "note" : (type === "note_entity" || type === "task_dependency") ? "task" : type; - const targetWorkspaceId = await resolveEdgeWorkspaceId(targetId, targetType); - if (targetWorkspaceId) { - await requireWorkspaceAccess(c, targetWorkspaceId); - } - if (type === "note_link") { - await db.insert(noteLinks).values({ sourceNoteId: sourceId, targetNoteId: targetId }); - } else if (type === "note_entity") { - await db.insert(noteEntityLinks).values({ noteId: sourceId, entityType: "task", entityId: targetId }); - } else if (type === "task_dependency") { - await db.insert(taskDependencies).values({ taskId: sourceId, dependsOnTaskId: targetId }); - } else { - return c.json({ error: { code: "VALIDATION_ERROR", message: "Unknown edge type: " + type } }, 400); - } + await db.insert(links).values({ + sourceType, + sourceId, + targetType, + targetId, + linkType: type as any, + }); if (!workspaceId) { - console.warn(`[graph] POST /edges: could not resolve workspace for source ${sourceId} (type ${type}); skipping activity`); + console.warn(`[graph] POST /edges: could not resolve workspace for source ${sourceId} (type ${sourceType}); skipping activity`); } else { await recordActivity({ actor: user.name, @@ -195,39 +168,20 @@ graphRoutes.post("/edges", async (c) => { } }); -// DELETE /api/graph/edges/:id — Remove +// DELETE /api/graph/edges/:id — Remove via links table graphRoutes.delete("/edges/:id", async (c) => { try { const user = await requireAuth(c); const id = c.req.param("id"); const [sourceId, targetId] = id.split("-"); - // The type isn't known at delete time, so resolve from the source entity: - // it's either a note or a task. Verify ownership before mutating anything. - let workspaceId = await resolveEdgeWorkspaceId(sourceId, "note"); - if (!workspaceId) { - workspaceId = await resolveEdgeWorkspaceId(sourceId, "task"); - } + const workspaceId = await resolveEdgeWorkspaceId(sourceId, "note") || await resolveEdgeWorkspaceId(sourceId, "task"); if (workspaceId) { await requireWorkspaceAccess(c, workspaceId); } - // Try deleting from note_links first - const result = await db.delete(noteLinks) - .where(and(eq(noteLinks.sourceNoteId, sourceId), eq(noteLinks.targetNoteId, targetId))) - .returning(); - - if (result.length === 0) { - // Try note_entity_links (note → entity edges) - const entityResult = await db.delete(noteEntityLinks) - .where(and(eq(noteEntityLinks.noteId, sourceId), eq(noteEntityLinks.entityId, targetId))) - .returning(); - if (entityResult.length === 0) { - // Try task_dependencies - await db.delete(taskDependencies) - .where(and(eq(taskDependencies.taskId, sourceId), eq(taskDependencies.dependsOnTaskId, targetId))); - } - } + await db.delete(links) + .where(and(eq(links.sourceId, sourceId), eq(links.targetId, targetId))); if (!workspaceId) { console.warn(`[graph] DELETE /edges/${id}: could not resolve workspace for source ${sourceId}; skipping activity`); @@ -251,3 +205,15 @@ graphRoutes.delete("/edges/:id", async (c) => { return c.json({ error: { code: "INTERNAL_ERROR", message: "Failed to delete edge" } }, 500); } }); + +async function resolveEdgeWorkspaceId(sourceId: string, type: string): Promise { + if (type === "note") { + const [row] = await db.select({ domainId: notes.domainId }).from(notes).where(eq(notes.id, sourceId)).limit(1); + return row?.domainId ?? null; + } + if (type === "task") { + const [row] = await db.select({ domainId: tasks.domainId }).from(tasks).where(eq(tasks.id, sourceId)).limit(1); + return row?.domainId ?? null; + } + return null; +} diff --git a/apps/api/src/routes/mcp.ts b/apps/api/src/routes/mcp.ts index 039fe37..0cacc62 100644 --- a/apps/api/src/routes/mcp.ts +++ b/apps/api/src/routes/mcp.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { createHash } from "node:crypto"; -import { db, apiKeys, users, tasks, taskTags, tags as tagsTable, habits, habitCompletions, projects, notes, noteLinks, domains, activityFeed, webhooks, webhookDeliveries } from "@project-e/db"; +import { db, apiKeys, users, tasks, taskTags, tags as tagsTable, habits, habitCompletions, projects, notes, domains, activityFeed, webhooks, webhookDeliveries } from "@project-e/db"; import { and, asc, desc, eq, ilike, isNull, or } from "drizzle-orm"; import { recordActivity } from "../middleware/activity"; @@ -95,7 +95,7 @@ const tools: ToolDefinition[] = [ eq(tasks.domainId, params.domain_id as string), isNull(tasks.deletedAt), ]; - if (params.status) conditions.push(eq(tasks.status, params.status as any)); + // TODO(phase-2): filter by state_group / state_id instead of old status if (params.priority) conditions.push(eq(tasks.priority, params.priority as any)); if (params.project_id) conditions.push(eq(tasks.projectId, params.project_id as string)); if (params.search) conditions.push(ilike(tasks.title, `%${params.search}%`)); @@ -119,7 +119,7 @@ const tools: ToolDefinition[] = [ domain_id: { type: "string", description: "Workspace/domain ID" }, title: { type: "string" }, description: { type: "string" }, - status: { type: "string", enum: ["todo", "in_progress", "done", "cancelled"] }, + status: { type: "string" }, priority: { type: "string", enum: ["low", "medium", "high", "urgent"] }, due_date: { type: "string" }, project_id: { type: "string" }, @@ -130,7 +130,6 @@ const tools: ToolDefinition[] = [ const [task] = await db.insert(tasks).values({ title: params.title as string, description: (params.description as string) ?? null, - status: (params.status as any) ?? "todo", priority: (params.priority as any) ?? "medium", domainId: params.domain_id as string, projectId: (params.project_id as string) ?? null, @@ -142,7 +141,7 @@ const tools: ToolDefinition[] = [ action: "created", entityType: "task", entityId: task.id, - changes: { title: task.title, status: task.status }, + changes: { title: task.title }, workspaceId: params.domain_id as string, }); @@ -158,7 +157,7 @@ const tools: ToolDefinition[] = [ task_id: { type: "string" }, title: { type: "string" }, description: { type: "string" }, - status: { type: "string", enum: ["todo", "in_progress", "done", "cancelled"] }, + status: { type: "string" }, priority: { type: "string", enum: ["low", "medium", "high", "urgent"] }, due_date: { type: "string" }, }, @@ -172,7 +171,6 @@ const tools: ToolDefinition[] = [ const updateData: Record = {}; if (params.title !== undefined) updateData.title = params.title; if (params.description !== undefined) updateData.description = params.description; - if (params.status !== undefined) updateData.status = params.status; if (params.priority !== undefined) updateData.priority = params.priority; if (params.due_date !== undefined) updateData.dueDate = params.due_date ? new Date(params.due_date as string) : null; updateData.updatedAt = new Date(); @@ -237,7 +235,7 @@ const tools: ToolDefinition[] = [ await verifyDomainAccess(existing.domainId, auth.userId); const [task] = await db.update(tasks) - .set({ status: "done", completedAt: new Date(), updatedAt: new Date() }) + .set({ completedAt: new Date(), updatedAt: new Date() }) .where(and(eq(tasks.id, params.task_id as string), isNull(tasks.deletedAt))) .returning(); @@ -566,7 +564,7 @@ const tools: ToolDefinition[] = [ const results: Record = {}; if (types.includes("tasks")) { - results.tasks = await db.select({ id: tasks.id, title: tasks.title, status: tasks.status, priority: tasks.priority }).from(tasks) + results.tasks = await db.select({ id: tasks.id, title: tasks.title, priority: tasks.priority }).from(tasks) .where(and(eq(tasks.domainId, domainId), isNull(tasks.deletedAt), ilike(tasks.title, `%${query}%`))).limit(limit); } if (types.includes("notes")) { diff --git a/apps/api/src/routes/note-link-service.ts b/apps/api/src/routes/note-link-service.ts index 070da31..304e8b2 100644 --- a/apps/api/src/routes/note-link-service.ts +++ b/apps/api/src/routes/note-link-service.ts @@ -1,13 +1,13 @@ /** * Note Link Service * - * Handles wikilink resolution and note_links / note_entity_links management. + * Handles wikilink resolution and links management. * On note save, parses content for [[wikilinks]], resolves each to a note_id or entity_id, * and diffs the existing links to produce idempotent deletes+inserts. */ -import { db, noteLinks, noteEntityLinks, notes, tasks, habits, projects, sections, tags as tagsTable } from "@project-e/db"; -import { and, eq, inArray, isNull, desc } from "drizzle-orm"; +import { db, links, notes, tasks, habits, projects, sections, tags as tagsTable } from "@project-e/db"; +import { and, eq, inArray, isNull } from "drizzle-orm"; import { extractLinkTargets } from "./wikilink-parser"; /** @@ -103,59 +103,39 @@ export async function syncNoteLinks(noteId: string, content: string, domainId: s } } - const noteToNoteLinks = resolvedTargets.filter(t => t.entityType === "note"); - const entityLinks = resolvedTargets.filter(t => t.entityType !== "note"); + // --- Sync all links from this note via the canonical links table --- + const existingLinks = await db + .select({ targetId: links.targetId, targetType: links.targetType }) + .from(links) + .where(and(eq(links.sourceId, noteId), eq(links.sourceType, "note"))); - // --- Sync note_links --- - const existingNoteLinks = await db - .select({ targetNoteId: noteLinks.targetNoteId }) - .from(noteLinks) - .where(eq(noteLinks.sourceNoteId, noteId)); + const existingKeySet = new Set(existingLinks.map(l => `${l.targetType}:${l.targetId}`)); + const newKeySet = new Set(resolvedTargets.map(l => `${l.entityType}:${l.entityId}`)); - const existingTargetIds = new Set(existingNoteLinks.map(l => l.targetNoteId)); - const newTargetIds = new Set(noteToNoteLinks.map(l => l.entityId)); - - const staleTargetIds = [...existingTargetIds].filter(id => !newTargetIds.has(id)); - if (staleTargetIds.length > 0) { + // Delete stale links + const staleLinks = existingLinks.filter(l => !newKeySet.has(`${l.targetType}:${l.targetId}`)); + if (staleLinks.length > 0) { + const staleIds = staleLinks.map(l => l.targetId); await db - .delete(noteLinks) + .delete(links) .where(and( - eq(noteLinks.sourceNoteId, noteId), - inArray(noteLinks.targetNoteId, staleTargetIds), + eq(links.sourceId, noteId), + eq(links.sourceType, "note"), + inArray(links.targetId, staleIds), )); } - const missingTargetIds = [...newTargetIds].filter(id => !existingTargetIds.has(id)); - if (missingTargetIds.length > 0) { - await db.insert(noteLinks).values( - missingTargetIds.map(targetNoteId => ({ sourceNoteId: noteId, targetNoteId })) - ); - } - - // --- Sync note_entity_links --- - const existingEntityLinks = await db - .select({ entityType: noteEntityLinks.entityType, entityId: noteEntityLinks.entityId }) - .from(noteEntityLinks) - .where(eq(noteEntityLinks.noteId, noteId)); - - const existingEntityKeySet = new Set(existingEntityLinks.map(l => `${l.entityType}:${l.entityId}`)); - const newEntityKeySet = new Set(entityLinks.map(l => `${l.entityType}:${l.entityId}`)); - - const staleEntityLinks = existingEntityLinks.filter(l => !newEntityKeySet.has(`${l.entityType}:${l.entityId}`)); - for (const link of staleEntityLinks) { - await db - .delete(noteEntityLinks) - .where(and( - eq(noteEntityLinks.noteId, noteId), - eq(noteEntityLinks.entityType, link.entityType), - eq(noteEntityLinks.entityId, link.entityId), - )); - } - - const missingEntityLinks = entityLinks.filter(l => !existingEntityKeySet.has(`${l.entityType}:${l.entityId}`)); - if (missingEntityLinks.length > 0) { - await db.insert(noteEntityLinks).values( - missingEntityLinks.map(l => ({ noteId, entityType: l.entityType, entityId: l.entityId })) + // Insert missing links + const missingTargets = resolvedTargets.filter(l => !existingKeySet.has(`${l.entityType}:${l.entityId}`)); + if (missingTargets.length > 0) { + await db.insert(links).values( + missingTargets.map(l => ({ + sourceType: "note", + sourceId: noteId, + targetType: l.entityType, + targetId: l.entityId, + linkType: "relates" as const, + })) ); } } @@ -170,10 +150,12 @@ export async function getBacklinks(noteId: string): Promise<{ id: string; title: title: notes.title, content: notes.content, }) - .from(noteLinks) - .innerJoin(notes, eq(noteLinks.sourceNoteId, notes.id)) + .from(links) + .innerJoin(notes, eq(links.sourceId, notes.id)) .where(and( - eq(noteLinks.targetNoteId, noteId), + eq(links.targetId, noteId), + eq(links.sourceType, "note"), + eq(links.targetType, "note"), isNull(notes.deletedAt), )); @@ -205,51 +187,52 @@ export async function getOutgoingLinks(noteId: string): Promise<{ noteLinks: { id: string; title: string }[]; entityLinks: { entityType: string; entityId: string; title: string | null }[]; }> { - const noteLinkRows = await db - .select({ id: notes.id, title: notes.title }) - .from(noteLinks) - .innerJoin(notes, eq(noteLinks.targetNoteId, notes.id)) - .where(and( - eq(noteLinks.sourceNoteId, noteId), - isNull(notes.deletedAt), - )); - - const entityLinkRows = await db - .select({ entityType: noteEntityLinks.entityType, entityId: noteEntityLinks.entityId }) - .from(noteEntityLinks) - .where(eq(noteEntityLinks.noteId, noteId)); + const outgoingLinks = await db + .select({ targetId: links.targetId, targetType: links.targetType }) + .from(links) + .where(and(eq(links.sourceId, noteId), eq(links.sourceType, "note"))); + const noteLinkRows: { id: string; title: string }[] = []; const entityLinksWithTitles: { entityType: string; entityId: string; title: string | null }[] = []; - for (const link of entityLinkRows) { - let title: string | null = null; - switch (link.entityType) { - case "task": { - const [t] = await db.select({ title: tasks.title }).from(tasks).where(eq(tasks.id, link.entityId)).limit(1); - title = t?.title ?? null; - break; - } - case "habit": { - const [h] = await db.select({ name: habits.name }).from(habits).where(eq(habits.id, link.entityId)).limit(1); - title = h?.name ?? null; - break; - } - case "project": { - const [p] = await db.select({ name: projects.name }).from(projects).where(eq(projects.id, link.entityId)).limit(1); - title = p?.name ?? null; - break; - } - case "section": { - const [s] = await db.select({ name: sections.name }).from(sections).where(eq(sections.id, link.entityId)).limit(1); - title = s?.name ?? null; - break; - } - case "tag": { - const [t] = await db.select({ name: tagsTable.name }).from(tagsTable).where(eq(tagsTable.id, link.entityId)).limit(1); - title = t?.name ?? null; - break; + + for (const link of outgoingLinks) { + if (link.targetType === "note") { + const [note] = await db.select({ id: notes.id, title: notes.title }) + .from(notes) + .where(and(eq(notes.id, link.targetId), isNull(notes.deletedAt))) + .limit(1); + if (note) noteLinkRows.push({ id: note.id, title: note.title }); + } else { + let title: string | null = null; + switch (link.targetType) { + case "task": { + const [t] = await db.select({ title: tasks.title }).from(tasks).where(eq(tasks.id, link.targetId)).limit(1); + title = t?.title ?? null; + break; + } + case "habit": { + const [h] = await db.select({ name: habits.name }).from(habits).where(eq(habits.id, link.targetId)).limit(1); + title = h?.name ?? null; + break; + } + case "project": { + const [p] = await db.select({ name: projects.name }).from(projects).where(eq(projects.id, link.targetId)).limit(1); + title = p?.name ?? null; + break; + } + case "section": { + const [s] = await db.select({ name: sections.name }).from(sections).where(eq(sections.id, link.targetId)).limit(1); + title = s?.name ?? null; + break; + } + case "tag": { + const [t] = await db.select({ name: tagsTable.name }).from(tagsTable).where(eq(tagsTable.id, link.targetId)).limit(1); + title = t?.name ?? null; + break; + } } + entityLinksWithTitles.push({ entityType: link.targetType, entityId: link.targetId, title }); } - entityLinksWithTitles.push({ entityType: link.entityType, entityId: link.entityId, title }); } return { diff --git a/apps/api/src/routes/projects.ts b/apps/api/src/routes/projects.ts index 3ea3294..62770e5 100644 --- a/apps/api/src/routes/projects.ts +++ b/apps/api/src/routes/projects.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; -import { db, projects, tasks, sections, projectTags, tags as tagsTable, activityFeed } from "@project-e/db"; -import { and, asc, desc, eq, ilike, inArray, isNull, sql } from "drizzle-orm"; +import { db, projects, tasks, sections, projectTags, tags as tagsTable, activityFeed, states } from "@project-e/db"; +import { and, asc, desc, eq, ilike, inArray, isNull, isNotNull, sql } from "drizzle-orm"; import { requireAuth, requireWorkspaceAccess, createErrorResponse, resolveActiveDomain, AuthError, isUuid } from "../middleware/auth"; import { recordActivity } from "../middleware/activity"; import { enqueueWebhooks } from "../middleware/webhook-queue"; @@ -144,7 +144,7 @@ projectRoutes.get("/", async (c) => { const [completedResult] = await db.select({ count: sql`count(*)` }) .from(tasks) - .where(and(eq(tasks.projectId, projectId), eq(tasks.status, "done"), isNull(tasks.deletedAt))); + .where(and(eq(tasks.projectId, projectId), isNotNull(tasks.completedAt), isNull(tasks.deletedAt))); taskCountMap.set(projectId, { total: Number(totalResult?.count || 0), @@ -210,6 +210,24 @@ projectRoutes.post("/", async (c) => { ); } + // Seed 5 default workflow states for the new project (Decision 10) + const defaultStates = [ + { name: 'Backlog', color: '#94a3b8', group: 'backlog' as const, sortOrder: 0 }, + { name: 'Todo', color: '#60a5fa', group: 'unstarted' as const, sortOrder: 1 }, + { name: 'In Progress', color: '#facc15', group: 'started' as const, sortOrder: 2 }, + { name: 'Done', color: '#4ade80', group: 'completed' as const, sortOrder: 3 }, + { name: 'Cancelled', color: '#f87171', group: 'cancelled' as const, sortOrder: 4 }, + ]; + await db.insert(states).values( + defaultStates.map(s => ({ + name: s.name, + color: s.color, + group: s.group, + sortOrder: s.sortOrder, + projectId: project.id, + })) + ); + await recordActivity({ actor: user.name, action: "created", @@ -277,7 +295,7 @@ projectRoutes.get("/:id", async (c) => { .where(eq(projectTags.projectId, id)); const totalTasks = projectTasks.length; - const completedTasks = projectTasks.filter(t => t.status === "done").length; + const completedTasks = projectTasks.filter(t => t.completedAt !== null).length; const progress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; return c.json({ diff --git a/apps/api/src/routes/tasks.ts b/apps/api/src/routes/tasks.ts index 96aa63d..77b5e74 100644 --- a/apps/api/src/routes/tasks.ts +++ b/apps/api/src/routes/tasks.ts @@ -1,5 +1,5 @@ import { Hono } from "hono"; -import { db, tasks, taskTags, tags as tagsTable, taskDependencies, activityFeed, scheduledJobs, projects, sections } from "@project-e/db"; +import { db, tasks, taskTags, tags as tagsTable, activityFeed, scheduledJobs, projects, sections, links } from "@project-e/db"; import { and, asc, desc, eq, exists, ilike, inArray, isNull, or, sql } from "drizzle-orm"; import { requireAuth, requireWorkspaceAccess, createErrorResponse, resolveActiveDomain, AuthError, isUuid } from "../middleware/auth"; import { recordActivity } from "../middleware/activity"; @@ -9,17 +9,18 @@ import { RRule } from "rrule"; export const taskRoutes = new Hono(); -const taskStatusEnum = z.enum(["todo", "in_progress", "done", "cancelled"]); const taskPriorityEnum = z.enum(["low", "medium", "high", "urgent"]); const createTaskSchema = z.object({ title: z.string().min(1, "Title is required"), description: z.string().optional().nullable(), - status: taskStatusEnum.optional().default("todo"), priority: taskPriorityEnum.optional().default("medium"), domain: z.string().min(1, "Domain is required"), projectId: z.string().uuid().optional().nullable(), sectionId: z.string().uuid().optional().nullable(), + stateId: z.string().uuid().optional().nullable(), + moduleId: z.string().uuid().optional().nullable(), + cycleId: z.string().uuid().optional().nullable(), parentId: z.string().uuid().optional().nullable(), dueDate: z.string().datetime().optional().nullable(), estimatedMinutes: z.number().int().positive().optional().nullable(), @@ -33,10 +34,12 @@ const createTaskSchema = z.object({ const updateTaskSchema = z.object({ title: z.string().min(1).optional(), description: z.string().optional().nullable(), - status: taskStatusEnum.optional(), priority: taskPriorityEnum.optional(), projectId: z.string().uuid().optional().nullable(), sectionId: z.string().uuid().optional().nullable(), + stateId: z.string().uuid().optional().nullable(), + moduleId: z.string().uuid().optional().nullable(), + cycleId: z.string().uuid().optional().nullable(), parentId: z.string().uuid().optional().nullable(), dueDate: z.string().datetime().optional().nullable(), estimatedMinutes: z.number().int().positive().optional().nullable(), @@ -86,7 +89,7 @@ taskRoutes.get("/", async (c) => { const perPage = Math.min(100, Math.max(1, parseInt(url.searchParams.get("perPage") || "50"))); const filter = url.searchParams.get("filter") || undefined; const sort = url.searchParams.get("sort") || "-created"; - const status = url.searchParams.get("status"); + const stateId = url.searchParams.get("state_id"); const priority = url.searchParams.get("priority"); const tag = url.searchParams.get("tag"); const search = url.searchParams.get("search"); @@ -111,9 +114,9 @@ taskRoutes.get("/", async (c) => { isNull(tasks.deletedAt), ]; - if (status) { - const statuses = status.split(","); - conditions.push(inArray(tasks.status, statuses as any)); + if (stateId) { + const stateIds = stateId.split(","); + conditions.push(inArray(tasks.stateId, stateIds)); } if (priority) { const priorities = priority.split(","); @@ -165,7 +168,6 @@ taskRoutes.get("/", async (c) => { created: tasks.createdAt, updated: tasks.updatedAt, title: tasks.title, - status: tasks.status, priority: tasks.priority, order: tasks.order, due_date: tasks.dueDate, @@ -297,11 +299,13 @@ taskRoutes.post("/", async (c) => { const [task] = await db.insert(tasks).values({ title: data.title, description: data.description ?? null, - status: data.status, priority: data.priority, domainId: data.domain, projectId: data.projectId ?? null, sectionId: data.sectionId ?? null, + stateId: data.stateId ?? null, + moduleId: data.moduleId ?? null, + cycleId: data.cycleId ?? null, parentId: data.parentId ?? null, dueDate: data.dueDate ? new Date(data.dueDate) : null, estimatedMinutes: data.estimatedMinutes ?? null, @@ -334,7 +338,7 @@ taskRoutes.post("/", async (c) => { action: "created", entityType: "task", entityId: task.id, - changes: { title: task.title, status: task.status, priority: task.priority }, + changes: { title: task.title, priority: task.priority }, workspaceId: data.domain, }); @@ -458,25 +462,9 @@ taskRoutes.get("/:id", async (c) => { .innerJoin(tagsTable, eq(taskTags.tagId, tagsTable.id)) .where(eq(taskTags.taskId, id)); - // Fetch dependencies (tasks this task depends on) - const depRows = await db.select({ - id: tasks.id, - title: tasks.title, - status: tasks.status, - }) - .from(taskDependencies) - .innerJoin(tasks, eq(taskDependencies.dependsOnTaskId, tasks.id)) - .where(and(eq(taskDependencies.taskId, id), isNull(tasks.deletedAt))); - - // Fetch dependents (tasks that depend on this task) - const dependentRows = await db.select({ - id: tasks.id, - title: tasks.title, - status: tasks.status, - }) - .from(taskDependencies) - .innerJoin(tasks, eq(taskDependencies.taskId, tasks.id)) - .where(and(eq(taskDependencies.dependsOnTaskId, id), isNull(tasks.deletedAt))); + // Dependencies are now managed via the links table (Phase 2) + const depRows: { id: string; title: string }[] = []; + const dependentRows: { id: string; title: string }[] = []; return c.json({ ...task, @@ -539,10 +527,12 @@ taskRoutes.patch("/:id", async (c) => { const updateValues: Record = {}; if (data.title !== undefined) updateValues.title = data.title; if (data.description !== undefined) updateValues.description = data.description; - if (data.status !== undefined) updateValues.status = data.status; if (data.priority !== undefined) updateValues.priority = data.priority; if (data.projectId !== undefined) updateValues.projectId = data.projectId; if (data.sectionId !== undefined) updateValues.sectionId = data.sectionId; + if (data.stateId !== undefined) updateValues.stateId = data.stateId; + if (data.moduleId !== undefined) updateValues.moduleId = data.moduleId; + if (data.cycleId !== undefined) updateValues.cycleId = data.cycleId; if (data.parentId !== undefined) updateValues.parentId = data.parentId; if (data.dueDate !== undefined) updateValues.dueDate = data.dueDate ? new Date(data.dueDate) : null; if (data.estimatedMinutes !== undefined) updateValues.estimatedMinutes = data.estimatedMinutes; @@ -561,11 +551,11 @@ taskRoutes.patch("/:id", async (c) => { action: "updated", entityType: "task", entityId: id, - changes: { ...data, previousStatus: existing.status }, + changes: { ...data, previousStateId: existing.stateId }, workspaceId: existing.domainId, }); - await enqueueWebhooks({ workspaceId: existing.domainId, event: "task.updated", entityType: "task", entityId: id, data: { ...data, previousStatus: existing.status } }); + await enqueueWebhooks({ workspaceId: existing.domainId, event: "task.updated", entityType: "task", entityId: id, data: { ...data, previousStateId: existing.stateId } }); if (data.recurrenceRule !== undefined) { await syncScheduledJob(id, data.recurrenceRule); @@ -728,187 +718,19 @@ taskRoutes.delete("/:id/tags/:tagId", async (c) => { } }); -// POST /api/tasks/:id/dependencies — Make this task depend on another task +// POST /api/tasks/:id/dependencies — Deprecated: use links table instead (Phase 2) taskRoutes.post("/:id/dependencies", async (c) => { - try { - const user = await requireAuth(c); - const id = c.req.param("id"); - if (!isUuid(id)) { - return c.json({ error: { code: "NOT_FOUND", message: "Resource not found" } }, 404); - } - const body = await c.req.json(); - const { dependsOnTaskId } = z.object({ - dependsOnTaskId: z.string().uuid("Invalid task id"), - }).parse(body); - - const [task] = await db.select({ id: tasks.id, domainId: tasks.domainId }) - .from(tasks) - .where(and(eq(tasks.id, id), isNull(tasks.deletedAt))) - .limit(1); - if (!task) { - return c.json({ error: { code: "NOT_FOUND", message: "Task not found" } }, 404); - } - - await requireWorkspaceAccess(c, task.domainId); - - // A task cannot depend on itself - if (dependsOnTaskId === id) { - return c.json({ error: { code: "VALIDATION_ERROR", message: "A task cannot depend on itself" } }, 400); - } - - const [depTask] = await db.select({ id: tasks.id, domainId: tasks.domainId }) - .from(tasks) - .where(and(eq(tasks.id, dependsOnTaskId), isNull(tasks.deletedAt))) - .limit(1); - if (!depTask) { - return c.json({ error: { code: "NOT_FOUND", message: "Dependency task not found" } }, 404); - } - if (depTask.domainId !== task.domainId) { - return c.json({ error: { code: "FORBIDDEN", message: "Dependency task does not belong to this workspace" } }, 403); - } - - // Cycle guard: walk the dependency chain (X depends on Y, Y on Z, ...) from - // dependsOnTaskId; reaching id means adding this edge would create a cycle. - let currentId: string | null = dependsOnTaskId; - const visited = new Set([id]); - while (currentId) { - if (visited.has(currentId)) { - return c.json({ error: { code: "VALIDATION_ERROR", message: "Circular dependency detected" } }, 400); - } - visited.add(currentId); - const [next] = await db.select({ dependsOnTaskId: taskDependencies.dependsOnTaskId }) - .from(taskDependencies) - .where(eq(taskDependencies.taskId, currentId)) - .limit(1); - currentId = next?.dependsOnTaskId ?? null; - } - - // Junction table has a composite PK — ignore duplicate edges - await db.insert(taskDependencies).values({ taskId: id, dependsOnTaskId }).onConflictDoNothing(); - - await recordActivity({ - actor: user.name, - action: "dependency_added", - entityType: "task", - entityId: id, - changes: { dependsOnTaskId }, - workspaceId: task.domainId, - }); - - await enqueueWebhooks({ workspaceId: task.domainId, event: "task.updated", entityType: "task", entityId: id, data: { dependsOnTaskId } }); - - return c.json({ success: true }, 201); - } catch (error) { - if (error instanceof AuthError) { - return c.json({ error: { code: error.code, message: error.message } }, error.status as any); - } - if (error instanceof z.ZodError) { - return c.json({ error: { code: "VALIDATION_ERROR", message: "Invalid input", details: error.issues } }, 400); - } - console.error("[tasks] POST /:id/dependencies error:", error); - return c.json({ error: { code: "INTERNAL_ERROR", message: "Failed to add dependency" } }, 500); - } + return c.json({ error: { code: "NOT_FOUND", message: "Dependencies moved to links table (Phase 2)" } }, 404); }); -// DELETE /api/tasks/:id/dependencies/:depId — Remove a dependency +// DELETE /api/tasks/:id/dependencies/:depId — Deprecated: use links table instead (Phase 2) taskRoutes.delete("/:id/dependencies/:depId", async (c) => { - try { - const user = await requireAuth(c); - const id = c.req.param("id"); - if (!isUuid(id)) { - return c.json({ error: { code: "NOT_FOUND", message: "Resource not found" } }, 404); - } - const depId = c.req.param("depId"); - - const [task] = await db.select({ id: tasks.id, domainId: tasks.domainId }) - .from(tasks) - .where(and(eq(tasks.id, id), isNull(tasks.deletedAt))) - .limit(1); - if (!task) { - return c.json({ error: { code: "NOT_FOUND", message: "Task not found" } }, 404); - } - - await requireWorkspaceAccess(c, task.domainId); - - // Junction table has no deleted_at — hard delete is correct here - await db.delete(taskDependencies).where(and(eq(taskDependencies.taskId, id), eq(taskDependencies.dependsOnTaskId, depId))); - - await recordActivity({ - actor: user.name, - action: "dependency_removed", - entityType: "task", - entityId: id, - changes: { removedDependsOnTaskId: depId }, - workspaceId: task.domainId, - }); - - return c.body(null, 204); - } catch (error) { - if (error instanceof AuthError) { - return c.json({ error: { code: error.code, message: error.message } }, error.status as any); - } - console.error("[tasks] DELETE /:id/dependencies/:depId error:", error); - return c.json({ error: { code: "INTERNAL_ERROR", message: "Failed to remove dependency" } }, 500); - } + return c.json({ error: { code: "NOT_FOUND", message: "Dependencies moved to links table (Phase 2)" } }, 404); }); -// POST /api/tasks/:id/status — Change task status (Kanban drag) +// POST /api/tasks/:id/status — Deprecated: use state_id instead (Phase 2) taskRoutes.post("/:id/status", async (c) => { - try { - const user = await requireAuth(c); - const id = c.req.param("id"); - if (!isUuid(id)) { - return c.json({ error: { code: "NOT_FOUND", message: "Resource not found" } }, 404); - } - const body = await c.req.json(); - const { status: newStatus } = z.object({ - status: taskStatusEnum, - }).parse(body); - - const [existing] = await db.select() - .from(tasks) - .where(and(eq(tasks.id, id), isNull(tasks.deletedAt))) - .limit(1); - - if (!existing) { - return c.json({ error: { code: "NOT_FOUND", message: "Task not found" } }, 404); - } - - await requireWorkspaceAccess(c, existing.domainId); - - const updateValues: Record = { - status: newStatus, - updatedAt: new Date(), - }; - if (newStatus === "done") { - updateValues.completedAt = new Date(); - } - - const [updated] = await db.update(tasks) - .set(updateValues) - .where(eq(tasks.id, id)) - .returning(); - - await recordActivity({ - actor: user.name, - action: newStatus === "done" ? "completed" : "updated", - entityType: "task", - entityId: id, - changes: { previousStatus: existing.status, newStatus }, - workspaceId: existing.domainId, - }); - - return c.json(updated); - } catch (error) { - if (error instanceof AuthError) { - return c.json({ error: { code: error.code, message: error.message } }, error.status as any); - } - if (error instanceof z.ZodError) { - return c.json({ error: { code: "VALIDATION_ERROR", message: "Invalid input", details: error.issues } }, 400); - } - console.error("[tasks] POST /:id/status error:", error); - return c.json({ error: { code: "INTERNAL_ERROR", message: "Failed to update task status" } }, 500); - } + return c.json({ error: { code: "NOT_FOUND", message: "Status endpoint replaced by state assignment (Phase 2)" } }, 404); }); // GET /api/tasks/:id/history — Status change log (from activity feed) diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 4d2c8fc..d6e9b99 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -316,7 +316,6 @@ async function handleRecurringSpawn(job: typeof jobs.$inferSelect): Promise statement-breakpoint +DROP TABLE IF EXISTS "note_links" CASCADE;--> statement-breakpoint +DROP TABLE IF EXISTS "note_entity_links" CASCADE;--> statement-breakpoint + +-- ── Create new enums ─────────────────────────────────────────────────────────── +CREATE TYPE "state_group" AS ENUM ('backlog', 'unstarted', 'started', 'completed', 'cancelled');--> statement-breakpoint +CREATE TYPE "module_status" AS ENUM ('planned', 'in_progress', 'completed', 'cancelled');--> statement-breakpoint +CREATE TYPE "link_type" AS ENUM ('relates', 'blocks', 'parent-child', 'created-from');--> statement-breakpoint + +-- ── Create new tables ────────────────────────────────────────────────────────── +CREATE TABLE "states" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "color" text, + "group" "state_group" NOT NULL DEFAULT 'unstarted', + "project_id" uuid NOT NULL, + "sort_order" integer DEFAULT 0, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +);--> statement-breakpoint +CREATE INDEX "states_project_id_idx" ON "states" ("project_id");--> statement-breakpoint +CREATE INDEX "states_sort_order_idx" ON "states" ("project_id","sort_order");--> statement-breakpoint +CREATE TABLE "modules" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "description" text, + "project_id" uuid NOT NULL, + "status" "module_status" NOT NULL DEFAULT 'planned', + "start_date" timestamp with time zone, + "target_date" timestamp with time zone, + "sort_order" integer DEFAULT 0, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +);--> statement-breakpoint +CREATE INDEX "modules_project_id_idx" ON "modules" ("project_id");--> statement-breakpoint +CREATE TABLE "cycles" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "project_id" uuid NOT NULL, + "start_date" timestamp with time zone, + "end_date" timestamp with time zone, + "active" boolean DEFAULT false, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +);--> statement-breakpoint +CREATE INDEX "cycles_project_id_idx" ON "cycles" ("project_id");--> statement-breakpoint +CREATE TABLE "links" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "source_type" text NOT NULL, + "source_id" uuid NOT NULL, + "target_type" text NOT NULL, + "target_id" uuid NOT NULL, + "link_type" "link_type" NOT NULL, + "direction" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +);--> statement-breakpoint +CREATE INDEX "links_source_idx" ON "links" ("source_type","source_id");--> statement-breakpoint +CREATE INDEX "links_target_idx" ON "links" ("target_type","target_id");--> statement-breakpoint + +-- ── Modify tasks table ───────────────────────────────────────────────────────── +ALTER TABLE "tasks" ADD COLUMN "state_id" uuid;--> statement-breakpoint +ALTER TABLE "tasks" ADD COLUMN "module_id" uuid;--> statement-breakpoint +ALTER TABLE "tasks" ADD COLUMN "cycle_id" uuid;--> statement-breakpoint +ALTER TABLE "tasks" ADD CONSTRAINT "tasks_state_id_states_id_fk" FOREIGN KEY ("state_id") REFERENCES "states"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tasks" ADD CONSTRAINT "tasks_module_id_modules_id_fk" FOREIGN KEY ("module_id") REFERENCES "modules"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tasks" ADD CONSTRAINT "tasks_cycle_id_cycles_id_fk" FOREIGN KEY ("cycle_id") REFERENCES "cycles"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "tasks_state_id_idx" ON "tasks" ("state_id");--> statement-breakpoint +CREATE INDEX "tasks_module_id_idx" ON "tasks" ("module_id");--> statement-breakpoint +CREATE INDEX "tasks_cycle_id_idx" ON "tasks" ("cycle_id");--> statement-breakpoint +ALTER TABLE "tasks" DROP COLUMN "status";--> statement-breakpoint +DROP INDEX IF EXISTS "tasks_status_idx";--> statement-breakpoint + +-- ── Drop old enum type ───────────────────────────────────────────────────────── +DROP TYPE IF EXISTS "task_status" CASCADE; diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..6b46791 --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,4641 @@ +{ + "id": "bcd0bbd3-a263-43c5-95be-93d431171fdf", + "prevId": "5f43138d-c7d8-4deb-b039-db806525ef83", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_feed": { + "name": "activity_feed", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_feed_workspace_id_idx": { + "name": "activity_feed_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "activity_feed_entity_idx": { + "name": "activity_feed_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "activity_feed_created_at_idx": { + "name": "activity_feed_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "activity_feed_actor_idx": { + "name": "activity_feed_actor_idx", + "columns": [ + { + "expression": "actor", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "activity_feed_workspace_id_domains_id_fk": { + "name": "activity_feed_workspace_id_domains_id_fk", + "tableFrom": "activity_feed", + "columnsFrom": [ + "workspace_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_activity": { + "name": "agent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_activity_agent_id_idx": { + "name": "agent_activity_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "agent_activity_created_at_idx": { + "name": "agent_activity_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agent_activity_agent_id_agents_id_fk": { + "name": "agent_activity_agent_id_agents_id_fk", + "tableFrom": "agent_activity", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_tasks": { + "name": "agent_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_type": { + "name": "task_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_tasks_agent_id_idx": { + "name": "agent_tasks_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "agent_tasks_status_idx": { + "name": "agent_tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agent_tasks_agent_id_agents_id_fk": { + "name": "agent_tasks_agent_id_agents_id_fk", + "tableFrom": "agent_tasks", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "permission_tier": { + "name": "permission_tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read_only'" + }, + "custom_permissions": { + "name": "custom_permissions", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_domain_id_idx": { + "name": "agents_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "agents_status_idx": { + "name": "agents_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agents_domain_id_domains_id_fk": { + "name": "agents_domain_id_domains_id_fk", + "tableFrom": "agents", + "columnsFrom": [ + "domain_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_user_id_idx": { + "name": "api_keys_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "api_keys_key_hash_idx": { + "name": "api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "api_keys_active_idx": { + "name": "api_keys_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.calendar_events": { + "name": "calendar_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_time": { + "name": "start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "all_day": { + "name": "all_day", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "recurrence_rule": { + "name": "recurrence_rule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "calendar_events_domain_id_idx": { + "name": "calendar_events_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "calendar_events_start_time_idx": { + "name": "calendar_events_start_time_idx", + "columns": [ + { + "expression": "start_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "calendar_events_end_time_idx": { + "name": "calendar_events_end_time_idx", + "columns": [ + { + "expression": "end_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "calendar_events_domain_id_domains_id_fk": { + "name": "calendar_events_domain_id_domains_id_fk", + "tableFrom": "calendar_events", + "columnsFrom": [ + "domain_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.canvas_cards": { + "name": "canvas_cards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "canvas_id": { + "name": "canvas_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'note'" + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "x": { + "name": "x", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "y": { + "name": "y", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 200 + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 150 + }, + "rotation": { + "name": "rotation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "z_index": { + "name": "z_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "canvas_cards_canvas_id_idx": { + "name": "canvas_cards_canvas_id_idx", + "columns": [ + { + "expression": "canvas_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "canvas_cards_canvas_id_canvases_id_fk": { + "name": "canvas_cards_canvas_id_canvases_id_fk", + "tableFrom": "canvas_cards", + "columnsFrom": [ + "canvas_id" + ], + "tableTo": "canvases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.canvas_connections": { + "name": "canvas_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "canvas_id": { + "name": "canvas_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_card_id": { + "name": "source_card_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_card_id": { + "name": "target_card_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "style": { + "name": "style", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'solid'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "canvas_connections_canvas_id_idx": { + "name": "canvas_connections_canvas_id_idx", + "columns": [ + { + "expression": "canvas_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "canvas_connections_canvas_id_canvases_id_fk": { + "name": "canvas_connections_canvas_id_canvases_id_fk", + "tableFrom": "canvas_connections", + "columnsFrom": [ + "canvas_id" + ], + "tableTo": "canvases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "canvas_connections_source_card_id_canvas_cards_id_fk": { + "name": "canvas_connections_source_card_id_canvas_cards_id_fk", + "tableFrom": "canvas_connections", + "columnsFrom": [ + "source_card_id" + ], + "tableTo": "canvas_cards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "canvas_connections_target_card_id_canvas_cards_id_fk": { + "name": "canvas_connections_target_card_id_canvas_cards_id_fk", + "tableFrom": "canvas_connections", + "columnsFrom": [ + "target_card_id" + ], + "tableTo": "canvas_cards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.canvases": { + "name": "canvases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'freeform'" + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "viewport": { + "name": "viewport", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"x\":0,\"y\":0,\"zoom\":1}'::jsonb" + }, + "background": { + "name": "background", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "canvases_domain_id_idx": { + "name": "canvases_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "canvases_domain_id_domains_id_fk": { + "name": "canvases_domain_id_domains_id_fk", + "tableFrom": "canvases", + "columnsFrom": [ + "domain_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "comments_workspace_id_idx": { + "name": "comments_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "comments_entity_idx": { + "name": "comments_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "comments_parent_id_idx": { + "name": "comments_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "comments_created_at_idx": { + "name": "comments_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "comments_workspace_id_domains_id_fk": { + "name": "comments_workspace_id_domains_id_fk", + "tableFrom": "comments", + "columnsFrom": [ + "workspace_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "comments_parent_id_comments_id_fk": { + "name": "comments_parent_id_comments_id_fk", + "tableFrom": "comments", + "columnsFrom": [ + "parent_id" + ], + "tableTo": "comments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_fields": { + "name": "custom_fields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "default_value": { + "name": "default_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_fields_domain_id_idx": { + "name": "custom_fields_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "custom_fields_entity_type_idx": { + "name": "custom_fields_entity_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "custom_fields_domain_id_domains_id_fk": { + "name": "custom_fields_domain_id_domains_id_fk", + "tableFrom": "custom_fields", + "columnsFrom": [ + "domain_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.daily_notes": { + "name": "daily_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "energy": { + "name": "energy", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "daily_notes_domain_id_idx": { + "name": "daily_notes_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "daily_notes_date_idx": { + "name": "daily_notes_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "daily_notes_date_domain_idx": { + "name": "daily_notes_date_domain_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "daily_notes_domain_id_domains_id_fk": { + "name": "daily_notes_domain_id_domains_id_fk", + "tableFrom": "daily_notes", + "columnsFrom": [ + "domain_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_widgets": { + "name": "dashboard_widgets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "layout": { + "name": "layout", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"x\":0,\"y\":0,\"w\":2,\"h\":2}'::jsonb" + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dashboard_widgets_user_id_idx": { + "name": "dashboard_widgets_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "dashboard_widgets_domain_id_idx": { + "name": "dashboard_widgets_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "dashboard_widgets_user_id_users_id_fk": { + "name": "dashboard_widgets_user_id_users_id_fk", + "tableFrom": "dashboard_widgets", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "dashboard_widgets_domain_id_domains_id_fk": { + "name": "dashboard_widgets_domain_id_domains_id_fk", + "tableFrom": "dashboard_widgets", + "columnsFrom": [ + "domain_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domains": { + "name": "domains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "search_vector": { + "name": "search_vector", + "type": "tsvector", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "domains_parent_id_idx": { + "name": "domains_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "domains_slug_idx": { + "name": "domains_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "domains_search_idx": { + "name": "domains_search_idx", + "columns": [ + { + "expression": "search_vector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + } + }, + "foreignKeys": { + "domains_owner_id_users_id_fk": { + "name": "domains_owner_id_users_id_fk", + "tableFrom": "domains", + "columnsFrom": [ + "owner_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "domains_parent_id_domains_id_fk": { + "name": "domains_parent_id_domains_id_fk", + "tableFrom": "domains", + "columnsFrom": [ + "parent_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "domains_slug_unique": { + "name": "domains_slug_unique", + "columns": [ + "slug" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_logs": { + "name": "error_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'error'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stack_trace": { + "name": "stack_trace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "resolved": { + "name": "resolved", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "error_logs_level_idx": { + "name": "error_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "error_logs_created_at_idx": { + "name": "error_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "error_logs_resolved_idx": { + "name": "error_logs_resolved_idx", + "columns": [ + { + "expression": "resolved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.habit_completions": { + "name": "habit_completions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "habit_id": { + "name": "habit_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "habit_completions_habit_id_idx": { + "name": "habit_completions_habit_id_idx", + "columns": [ + { + "expression": "habit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "habit_completions_date_idx": { + "name": "habit_completions_date_idx", + "columns": [ + { + "expression": "habit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "habit_completions_habit_id_habits_id_fk": { + "name": "habit_completions_habit_id_habits_id_fk", + "tableFrom": "habit_completions", + "columnsFrom": [ + "habit_id" + ], + "tableTo": "habits", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.habit_tags": { + "name": "habit_tags", + "schema": "", + "columns": { + "habit_id": { + "name": "habit_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "habit_tags_tag_id_idx": { + "name": "habit_tags_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "habit_tags_habit_id_habits_id_fk": { + "name": "habit_tags_habit_id_habits_id_fk", + "tableFrom": "habit_tags", + "columnsFrom": [ + "habit_id" + ], + "tableTo": "habits", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "habit_tags_tag_id_tags_id_fk": { + "name": "habit_tags_tag_id_tags_id_fk", + "tableFrom": "habit_tags", + "columnsFrom": [ + "tag_id" + ], + "tableTo": "tags", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "habit_tags_habit_id_tag_id_pk": { + "name": "habit_tags_habit_id_tag_id_pk", + "columns": [ + "habit_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.habits": { + "name": "habits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "frequency": { + "name": "frequency", + "type": "habit_frequency", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'daily'" + }, + "difficulty": { + "name": "difficulty", + "type": "habit_difficulty", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "goal_per_period": { + "name": "goal_per_period", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reminder_time": { + "name": "reminder_time", + "type": "time", + "primaryKey": false, + "notNull": false + }, + "skip_days": { + "name": "skip_days", + "type": "integer[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "streak_count": { + "name": "streak_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "best_streak": { + "name": "best_streak", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "mood_tracking": { + "name": "mood_tracking", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "search_vector": { + "name": "search_vector", + "type": "tsvector", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "habits_domain_id_idx": { + "name": "habits_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "habits_active_idx": { + "name": "habits_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "habits_deleted_at_idx": { + "name": "habits_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "habits_search_idx": { + "name": "habits_search_idx", + "columns": [ + { + "expression": "search_vector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + } + }, + "foreignKeys": { + "habits_domain_id_domains_id_fk": { + "name": "habits_domain_id_domains_id_fk", + "tableFrom": "habits", + "columnsFrom": [ + "domain_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jobs": { + "name": "jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "jobs_status_idx": { + "name": "jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "jobs_type_idx": { + "name": "jobs_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "jobs_next_retry_at_idx": { + "name": "jobs_next_retry_at_idx", + "columns": [ + { + "expression": "next_retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.note_entity_links": { + "name": "note_entity_links", + "schema": "", + "columns": { + "note_id": { + "name": "note_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "note_entity_links_entity_idx": { + "name": "note_entity_links_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "note_entity_links_note_id_idx": { + "name": "note_entity_links_note_id_idx", + "columns": [ + { + "expression": "note_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "note_entity_links_note_id_notes_id_fk": { + "name": "note_entity_links_note_id_notes_id_fk", + "tableFrom": "note_entity_links", + "columnsFrom": [ + "note_id" + ], + "tableTo": "notes", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.note_links": { + "name": "note_links", + "schema": "", + "columns": { + "source_note_id": { + "name": "source_note_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_note_id": { + "name": "target_note_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "note_links_target_note_id_idx": { + "name": "note_links_target_note_id_idx", + "columns": [ + { + "expression": "target_note_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "note_links_source_note_id_notes_id_fk": { + "name": "note_links_source_note_id_notes_id_fk", + "tableFrom": "note_links", + "columnsFrom": [ + "source_note_id" + ], + "tableTo": "notes", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "note_links_target_note_id_notes_id_fk": { + "name": "note_links_target_note_id_notes_id_fk", + "tableFrom": "note_links", + "columnsFrom": [ + "target_note_id" + ], + "tableTo": "notes", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "note_links_source_note_id_target_note_id_pk": { + "name": "note_links_source_note_id_target_note_id_pk", + "columns": [ + "source_note_id", + "target_note_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.note_tags": { + "name": "note_tags", + "schema": "", + "columns": { + "note_id": { + "name": "note_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "note_tags_tag_id_idx": { + "name": "note_tags_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "note_tags_note_id_notes_id_fk": { + "name": "note_tags_note_id_notes_id_fk", + "tableFrom": "note_tags", + "columnsFrom": [ + "note_id" + ], + "tableTo": "notes", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "note_tags_tag_id_tags_id_fk": { + "name": "note_tags_tag_id_tags_id_fk", + "tableFrom": "note_tags", + "columnsFrom": [ + "tag_id" + ], + "tableTo": "tags", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "note_tags_note_id_tag_id_pk": { + "name": "note_tags_note_id_tag_id_pk", + "columns": [ + "note_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notes": { + "name": "notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_pinned": { + "name": "is_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "search_vector": { + "name": "search_vector", + "type": "tsvector", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "notes_domain_id_idx": { + "name": "notes_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "notes_is_pinned_idx": { + "name": "notes_is_pinned_idx", + "columns": [ + { + "expression": "is_pinned", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "notes_is_archived_idx": { + "name": "notes_is_archived_idx", + "columns": [ + { + "expression": "is_archived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "notes_deleted_at_idx": { + "name": "notes_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "notes_search_idx": { + "name": "notes_search_idx", + "columns": [ + { + "expression": "search_vector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + } + }, + "foreignKeys": { + "notes_domain_id_domains_id_fk": { + "name": "notes_domain_id_domains_id_fk", + "tableFrom": "notes", + "columnsFrom": [ + "domain_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_tags": { + "name": "project_tags", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "project_tags_tag_id_idx": { + "name": "project_tags_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "project_tags_project_id_projects_id_fk": { + "name": "project_tags_project_id_projects_id_fk", + "tableFrom": "project_tags", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "project_tags_tag_id_tags_id_fk": { + "name": "project_tags_tag_id_tags_id_fk", + "tableFrom": "project_tags", + "columnsFrom": [ + "tag_id" + ], + "tableTo": "tags", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "project_tags_project_id_tag_id_pk": { + "name": "project_tags_project_id_tag_id_pk", + "columns": [ + "project_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "project_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "search_vector": { + "name": "search_vector", + "type": "tsvector", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "projects_domain_id_idx": { + "name": "projects_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "projects_status_idx": { + "name": "projects_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "projects_deleted_at_idx": { + "name": "projects_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "projects_search_idx": { + "name": "projects_search_idx", + "columns": [ + { + "expression": "search_vector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + } + }, + "foreignKeys": { + "projects_domain_id_domains_id_fk": { + "name": "projects_domain_id_domains_id_fk", + "tableFrom": "projects", + "columnsFrom": [ + "domain_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Untitled report'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "date_range_start": { + "name": "date_range_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "date_range_end": { + "name": "date_range_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_draft": { + "name": "is_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reports_domain_idx": { + "name": "reports_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "reports_type_idx": { + "name": "reports_type_idx", + "columns": [ + { + "expression": "report_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "reports_created_at_idx": { + "name": "reports_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "reports_project_id_projects_id_fk": { + "name": "reports_project_id_projects_id_fk", + "tableFrom": "reports", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_jobs": { + "name": "scheduled_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recurrence_rule": { + "name": "recurrence_rule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "next_occurrence_at": { + "name": "next_occurrence_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_spawned_at": { + "name": "last_spawned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduled_jobs_next_occurrence_idx": { + "name": "scheduled_jobs_next_occurrence_idx", + "columns": [ + { + "expression": "next_occurrence_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "scheduled_jobs_entity_idx": { + "name": "scheduled_jobs_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sections": { + "name": "sections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "section_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'section'" + }, + "status": { + "name": "status", + "type": "section_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "target_date": { + "name": "target_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sections_project_id_idx": { + "name": "sections_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "sections_kind_idx": { + "name": "sections_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "sections_sort_order_idx": { + "name": "sections_sort_order_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "sections_project_id_projects_id_fk": { + "name": "sections_project_id_projects_id_fk", + "tableFrom": "sections", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "tag_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tags_parent_id_idx": { + "name": "tags_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tags_scope_idx": { + "name": "tags_scope_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tags_parent_id_tags_id_fk": { + "name": "tags_parent_id_tags_id_fk", + "tableFrom": "tags", + "columnsFrom": [ + "parent_id" + ], + "tableTo": "tags", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_dependencies": { + "name": "task_dependencies", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "depends_on_task_id": { + "name": "depends_on_task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "task_dependencies_depends_on_idx": { + "name": "task_dependencies_depends_on_idx", + "columns": [ + { + "expression": "depends_on_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "task_dependencies_task_id_tasks_id_fk": { + "name": "task_dependencies_task_id_tasks_id_fk", + "tableFrom": "task_dependencies", + "columnsFrom": [ + "task_id" + ], + "tableTo": "tasks", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "task_dependencies_depends_on_task_id_tasks_id_fk": { + "name": "task_dependencies_depends_on_task_id_tasks_id_fk", + "tableFrom": "task_dependencies", + "columnsFrom": [ + "depends_on_task_id" + ], + "tableTo": "tasks", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "task_dependencies_task_id_depends_on_task_id_pk": { + "name": "task_dependencies_task_id_depends_on_task_id_pk", + "columns": [ + "task_id", + "depends_on_task_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_tags": { + "name": "task_tags", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "task_tags_tag_id_idx": { + "name": "task_tags_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "task_tags_task_id_tasks_id_fk": { + "name": "task_tags_task_id_tasks_id_fk", + "tableFrom": "task_tags", + "columnsFrom": [ + "task_id" + ], + "tableTo": "tasks", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "task_tags_tag_id_tags_id_fk": { + "name": "task_tags_tag_id_tags_id_fk", + "tableFrom": "task_tags", + "columnsFrom": [ + "tag_id" + ], + "tableTo": "tags", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "task_tags_task_id_tag_id_pk": { + "name": "task_tags_task_id_tag_id_pk", + "columns": [ + "task_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'todo'" + }, + "priority": { + "name": "priority", + "type": "task_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "section_id": { + "name": "section_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tracked_minutes": { + "name": "tracked_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "recurrence_rule": { + "name": "recurrence_rule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "search_vector": { + "name": "search_vector", + "type": "tsvector", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tasks_domain_id_idx": { + "name": "tasks_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tasks_project_id_idx": { + "name": "tasks_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tasks_section_id_idx": { + "name": "tasks_section_id_idx", + "columns": [ + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tasks_parent_id_idx": { + "name": "tasks_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tasks_priority_idx": { + "name": "tasks_priority_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tasks_due_date_idx": { + "name": "tasks_due_date_idx", + "columns": [ + { + "expression": "due_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tasks_order_idx": { + "name": "tasks_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tasks_deleted_at_idx": { + "name": "tasks_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tasks_search_idx": { + "name": "tasks_search_idx", + "columns": [ + { + "expression": "search_vector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + } + }, + "foreignKeys": { + "tasks_domain_id_domains_id_fk": { + "name": "tasks_domain_id_domains_id_fk", + "tableFrom": "tasks", + "columnsFrom": [ + "domain_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tasks_section_id_sections_id_fk": { + "name": "tasks_section_id_sections_id_fk", + "tableFrom": "tasks", + "columnsFrom": [ + "section_id" + ], + "tableTo": "sections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tasks_parent_id_tasks_id_fk": { + "name": "tasks_parent_id_tasks_id_fk", + "tableFrom": "tasks", + "columnsFrom": [ + "parent_id" + ], + "tableTo": "tasks", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "passkey_credential_id": { + "name": "passkey_credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "passkey_public_key": { + "name": "passkey_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "passkey_counter": { + "name": "passkey_counter", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "webhook_id": { + "name": "webhook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_deliveries_webhook_id_idx": { + "name": "webhook_deliveries_webhook_id_idx", + "columns": [ + { + "expression": "webhook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "webhook_deliveries_status_idx": { + "name": "webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "webhook_deliveries_created_at_idx": { + "name": "webhook_deliveries_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "webhook_deliveries_webhook_id_webhooks_id_fk": { + "name": "webhook_deliveries_webhook_id_webhooks_id_fk", + "tableFrom": "webhook_deliveries", + "columnsFrom": [ + "webhook_id" + ], + "tableTo": "webhooks", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhooks": { + "name": "webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhooks_workspace_id_idx": { + "name": "webhooks_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "webhooks_active_idx": { + "name": "webhooks_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "webhooks_workspace_id_domains_id_fk": { + "name": "webhooks_workspace_id_domains_id_fk", + "tableFrom": "webhooks", + "columnsFrom": [ + "workspace_id" + ], + "tableTo": "domains", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.habit_difficulty": { + "name": "habit_difficulty", + "schema": "public", + "values": [ + "easy", + "medium", + "hard" + ] + }, + "public.habit_frequency": { + "name": "habit_frequency", + "schema": "public", + "values": [ + "daily", + "weekly", + "custom" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "pending", + "processing", + "completed", + "failed" + ] + }, + "public.project_status": { + "name": "project_status", + "schema": "public", + "values": [ + "active", + "paused", + "completed", + "archived" + ] + }, + "public.section_kind": { + "name": "section_kind", + "schema": "public", + "values": [ + "section", + "milestone" + ] + }, + "public.section_status": { + "name": "section_status", + "schema": "public", + "values": [ + "planned", + "in_progress", + "complete" + ] + }, + "public.tag_scope": { + "name": "tag_scope", + "schema": "public", + "values": [ + "global", + "tasks", + "habits", + "projects", + "notes" + ] + }, + "public.task_priority": { + "name": "task_priority", + "schema": "public", + "values": [ + "low", + "medium", + "high", + "urgent" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "todo", + "in_progress", + "done", + "cancelled" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..ff46e04 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,69 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "dialect": "postgresql", + "tag": "0000_outstanding_zuras", + "createdAt": "2025-01-01T00:00:00.000Z" + }, + { + "idx": 1, + "version": "7", + "dialect": "postgresql", + "tag": "0001_warm_eternity", + "createdAt": "2025-01-01T00:01:00.000Z" + }, + { + "idx": 2, + "version": "7", + "dialect": "postgresql", + "tag": "0002_steep_black_widow", + "createdAt": "2025-01-01T00:02:00.000Z" + }, + { + "idx": 3, + "version": "7", + "dialect": "postgresql", + "tag": "0003_amazing_saracen", + "createdAt": "2025-01-01T00:03:00.000Z" + }, + { + "idx": 4, + "version": "7", + "dialect": "postgresql", + "tag": "0004_add_owner_id_to_domains", + "createdAt": "2025-01-01T00:04:00.000Z" + }, + { + "idx": 5, + "version": "7", + "dialect": "postgresql", + "tag": "0005_search_vector_trigger", + "createdAt": "2025-01-01T00:05:00.000Z" + }, + { + "idx": 6, + "version": "7", + "dialect": "postgresql", + "tag": "0006_minor_doctor_octopus", + "createdAt": "2025-01-01T00:06:00.000Z" + }, + { + "idx": 7, + "version": "7", + "dialect": "postgresql", + "tag": "0007_drop_canvas_reports", + "createdAt": "2025-01-01T00:07:00.000Z" + }, + { + "idx": 8, + "version": "7", + "when": 1788800393149, + "tag": "0008_plane-lift-schema", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 7b8e363..00048f6 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -23,7 +23,6 @@ export const tsvector = customType<{ data: string }>({ // ── Enums ────────────────────────────────────────────────────────────────────── -export const taskStatusEnum = pgEnum('task_status', ['todo', 'in_progress', 'done', 'cancelled']); export const taskPriorityEnum = pgEnum('task_priority', ['low', 'medium', 'high', 'urgent']); export const habitFrequencyEnum = pgEnum('habit_frequency', ['daily', 'weekly', 'custom']); export const habitDifficultyEnum = pgEnum('habit_difficulty', ['easy', 'medium', 'hard']); @@ -32,6 +31,9 @@ export const sectionKindEnum = pgEnum('section_kind', ['section', 'milestone']); export const sectionStatusEnum = pgEnum('section_status', ['planned', 'in_progress', 'complete']); export const tagScopeEnum = pgEnum('tag_scope', ['global', 'tasks', 'habits', 'projects', 'notes']); export const jobStatusEnum = pgEnum('job_status', ['pending', 'processing', 'completed', 'failed']); +export const stateGroupEnum = pgEnum('state_group', ['backlog', 'unstarted', 'started', 'completed', 'cancelled']); +export const moduleStatusEnum = pgEnum('module_status', ['planned', 'in_progress', 'completed', 'cancelled']); +export const linkTypeEnum = pgEnum('link_type', ['relates', 'blocks', 'parent-child', 'created-from']); // ── Users ────────────────────────────────────────────────────────────────────── @@ -143,6 +145,72 @@ export const sections = pgTable( ] ); +// ── States (per-project workflow states) ──────────────────────────────────────── + +export const states = pgTable( + 'states', + { + id: uuid('id').defaultRandom().primaryKey(), + name: text('name').notNull(), + color: text('color'), + group: stateGroupEnum('group').notNull().default('unstarted'), + projectId: uuid('project_id') + .notNull() + .references((): any => projects.id, { onDelete: 'cascade' }), + sortOrder: integer('sort_order').default(0), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [ + index('states_project_id_idx').on(table.projectId), + index('states_sort_order_idx').on(table.projectId, table.sortOrder), + ] +); + +// ── Modules (project-scoped planning buckets) ────────────────────────────────── + +export const modules = pgTable( + 'modules', + { + id: uuid('id').defaultRandom().primaryKey(), + name: text('name').notNull(), + description: text('description'), + projectId: uuid('project_id') + .notNull() + .references((): any => projects.id, { onDelete: 'cascade' }), + status: moduleStatusEnum('status').notNull().default('planned'), + startDate: timestamp('start_date', { withTimezone: true }), + targetDate: timestamp('target_date', { withTimezone: true }), + sortOrder: integer('sort_order').default(0), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [ + index('modules_project_id_idx').on(table.projectId), + ] +); + +// ── Cycles (time-boxed sprints) ──────────────────────────────────────────────── + +export const cycles = pgTable( + 'cycles', + { + id: uuid('id').defaultRandom().primaryKey(), + name: text('name').notNull(), + projectId: uuid('project_id') + .notNull() + .references((): any => projects.id, { onDelete: 'cascade' }), + startDate: timestamp('start_date', { withTimezone: true }), + endDate: timestamp('end_date', { withTimezone: true }), + active: boolean('active').default(false), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [ + index('cycles_project_id_idx').on(table.projectId), + ] +); + // ── Tasks ─────────────────────────────────────────────────────────────────────── export const tasks = pgTable( @@ -151,13 +219,15 @@ export const tasks = pgTable( id: uuid('id').defaultRandom().primaryKey(), title: text('title').notNull(), description: text('description'), - status: taskStatusEnum('status').notNull().default('todo'), priority: taskPriorityEnum('priority').notNull().default('medium'), domainId: uuid('domain_id') .notNull() .references((): any => domains.id, { onDelete: 'cascade' }), projectId: uuid('project_id').references((): any => projects.id, { onDelete: 'set null' }), sectionId: uuid('section_id').references((): any => sections.id, { onDelete: 'set null' }), + stateId: uuid('state_id').references((): any => states.id, { onDelete: 'set null' }), + moduleId: uuid('module_id').references((): any => modules.id, { onDelete: 'set null' }), + cycleId: uuid('cycle_id').references((): any => cycles.id, { onDelete: 'set null' }), parentId: uuid('parent_id').references((): any => tasks.id, { onDelete: 'set null' }), dueDate: timestamp('due_date', { withTimezone: true }), completedAt: timestamp('completed_at', { withTimezone: true }), @@ -175,8 +245,10 @@ export const tasks = pgTable( index('tasks_domain_id_idx').on(table.domainId), index('tasks_project_id_idx').on(table.projectId), index('tasks_section_id_idx').on(table.sectionId), + index('tasks_state_id_idx').on(table.stateId), + index('tasks_module_id_idx').on(table.moduleId), + index('tasks_cycle_id_idx').on(table.cycleId), index('tasks_parent_id_idx').on(table.parentId), - index('tasks_status_idx').on(table.status), index('tasks_priority_idx').on(table.priority), index('tasks_due_date_idx').on(table.dueDate), index('tasks_order_idx').on(table.order), @@ -203,24 +275,6 @@ export const taskTags = pgTable( ] ); -// ── Task Dependencies (junction) ─────────────────────────────────────────────── - -export const taskDependencies = pgTable( - 'task_dependencies', - { - taskId: uuid('task_id') - .notNull() - .references((): any => tasks.id, { onDelete: 'cascade' }), - dependsOnTaskId: uuid('depends_on_task_id') - .notNull() - .references((): any => tasks.id, { onDelete: 'cascade' }), - }, - (table) => [ - primaryKey({ columns: [table.taskId, table.dependsOnTaskId] }), - index('task_dependencies_depends_on_idx').on(table.dependsOnTaskId), - ] -); - // ── Habits ────────────────────────────────────────────────────────────────────── export const habits = pgTable( @@ -321,41 +375,6 @@ export const notes = pgTable( ] ); -// ── Note Links (wikilinks / backlinks) ───────────────────────────────────────── - -export const noteLinks = pgTable( - 'note_links', - { - sourceNoteId: uuid('source_note_id') - .notNull() - .references((): any => notes.id, { onDelete: 'cascade' }), - targetNoteId: uuid('target_note_id') - .notNull() - .references((): any => notes.id, { onDelete: 'cascade' }), - }, - (table) => [ - primaryKey({ columns: [table.sourceNoteId, table.targetNoteId] }), - index('note_links_target_note_id_idx').on(table.targetNoteId), - ] -); - -// ── Note Entity Links (cross-entity linking) ──────────────────────────────────── - -export const noteEntityLinks = pgTable( - 'note_entity_links', - { - noteId: uuid('note_id') - .notNull() - .references((): any => notes.id, { onDelete: 'cascade' }), - entityType: text('entity_type').notNull(), - entityId: uuid('entity_id').notNull(), - }, - (table) => [ - index('note_entity_links_entity_idx').on(table.entityType, table.entityId), - index('note_entity_links_note_id_idx').on(table.noteId), - ] -); - // ── Note Tags (junction) ──────────────────────────────────────────────────────── export const noteTags = pgTable( @@ -374,6 +393,26 @@ export const noteTags = pgTable( ] ); +// ── Links (canonical cross-entity mesh) ──────────────────────────────────────── + +export const links = pgTable( + 'links', + { + id: uuid('id').defaultRandom().primaryKey(), + sourceType: text('source_type').notNull(), + sourceId: uuid('source_id').notNull(), + targetType: text('target_type').notNull(), + targetId: uuid('target_id').notNull(), + linkType: linkTypeEnum('link_type').notNull(), + direction: text('direction'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [ + index('links_source_idx').on(table.sourceType, table.sourceId), + index('links_target_idx').on(table.targetType, table.targetId), + ] +); + // ── Project Tags (junction) ───────────────────────────────────────────────────── export const projectTags = pgTable(