Files
hammer-queue/backend/src/db/schema.ts
Hammer dd2c80224e feat: add personal todos feature
- New todos table in DB schema (title, description, priority, category, due date, completion)
- Full CRUD + toggle API routes at /api/todos
- Categories support with filtering
- Bulk import endpoint for migration
- New TodosPage with inline editing, priority badges, due date display
- Add Todos to sidebar navigation
- Dark mode support throughout
2026-01-30 04:44:34 +00:00

253 lines
8.5 KiB
TypeScript

import {
pgTable,
uuid,
text,
integer,
timestamp,
jsonb,
pgEnum,
boolean,
} from "drizzle-orm/pg-core";
export const taskStatusEnum = pgEnum("task_status", [
"active",
"queued",
"blocked",
"completed",
"cancelled",
]);
export const taskPriorityEnum = pgEnum("task_priority", [
"critical",
"high",
"medium",
"low",
]);
export const taskSourceEnum = pgEnum("task_source", [
"donovan",
"david",
"hammer",
"heartbeat",
"cron",
"other",
]);
export interface ProgressNote {
timestamp: string;
note: string;
}
export interface Subtask {
id: string;
title: string;
completed: boolean;
completedAt?: string;
createdAt: string;
}
export type RecurrenceFrequency = "daily" | "weekly" | "biweekly" | "monthly";
export interface Recurrence {
frequency: RecurrenceFrequency;
/** Auto-activate the next instance (vs. queue it) */
autoActivate?: boolean;
}
// ─── Projects ───
export interface ProjectLink {
label: string;
url: string;
}
export const projects = pgTable("projects", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
description: text("description"),
context: text("context"), // Architecture notes, how-to, credentials references
repos: jsonb("repos").$type<string[]>().default([]), // Git repo URLs
links: jsonb("links").$type<ProjectLink[]>().default([]), // Related URLs (docs, domains, dashboards)
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export type Project = typeof projects.$inferSelect;
export type NewProject = typeof projects.$inferInsert;
// ─── Tasks ───
export const tasks = pgTable("tasks", {
id: uuid("id").defaultRandom().primaryKey(),
taskNumber: integer("task_number"),
title: text("title").notNull(),
description: text("description"),
source: taskSourceEnum("source").notNull().default("donovan"),
status: taskStatusEnum("status").notNull().default("queued"),
priority: taskPriorityEnum("priority").notNull().default("medium"),
position: integer("position").notNull().default(0),
assigneeId: text("assignee_id"),
assigneeName: text("assignee_name"),
projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }),
dueDate: timestamp("due_date", { withTimezone: true }),
estimatedHours: integer("estimated_hours"),
tags: jsonb("tags").$type<string[]>().default([]),
recurrence: jsonb("recurrence").$type<Recurrence>(),
subtasks: jsonb("subtasks").$type<Subtask[]>().default([]),
progressNotes: jsonb("progress_notes").$type<ProgressNote[]>().default([]),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
completedAt: timestamp("completed_at", { withTimezone: true }),
});
export type Task = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;
// ─── Comments ───
export const taskComments = pgTable("task_comments", {
id: uuid("id").defaultRandom().primaryKey(),
taskId: uuid("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
authorId: text("author_id"), // BetterAuth user ID, or "hammer" for API, null for anonymous
authorName: text("author_name").notNull(),
content: text("content").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
export type TaskComment = typeof taskComments.$inferSelect;
export type NewTaskComment = typeof taskComments.$inferInsert;
// ─── Security Audits ───
export const securityAuditStatusEnum = pgEnum("security_audit_status", [
"strong",
"needs_improvement",
"critical",
]);
export interface SecurityFinding {
id: string;
status: "strong" | "needs_improvement" | "critical";
title: string;
description: string;
recommendation: string;
}
export const securityAudits = pgTable("security_audits", {
id: uuid("id").defaultRandom().primaryKey(),
projectName: text("project_name").notNull(),
category: text("category").notNull(),
findings: jsonb("findings").$type<SecurityFinding[]>().default([]),
score: integer("score").notNull().default(0), // 0-100
lastAudited: timestamp("last_audited", { withTimezone: true }).defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export type SecurityAudit = typeof securityAudits.$inferSelect;
export type NewSecurityAudit = typeof securityAudits.$inferInsert;
// ─── Daily Summaries ───
export interface SummaryHighlight {
text: string;
}
export interface SummaryStats {
deploys?: number;
commits?: number;
tasksCompleted?: number;
featuresBuilt?: number;
bugsFixed?: number;
[key: string]: number | undefined;
}
export const dailySummaries = pgTable("daily_summaries", {
id: uuid("id").defaultRandom().primaryKey(),
date: text("date").notNull().unique(), // YYYY-MM-DD
content: text("content").notNull(),
highlights: jsonb("highlights").$type<SummaryHighlight[]>().default([]),
stats: jsonb("stats").$type<SummaryStats>().default({}),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export type DailySummary = typeof dailySummaries.$inferSelect;
export type NewDailySummary = typeof dailySummaries.$inferInsert;
// ─── Personal Todos ───
export const todoPriorityEnum = pgEnum("todo_priority", [
"high",
"medium",
"low",
"none",
]);
export const todos = pgTable("todos", {
id: uuid("id").defaultRandom().primaryKey(),
userId: text("user_id").notNull(),
title: text("title").notNull(),
description: text("description"),
isCompleted: boolean("is_completed").notNull().default(false),
priority: todoPriorityEnum("priority").notNull().default("none"),
category: text("category"),
dueDate: timestamp("due_date", { withTimezone: true }),
completedAt: timestamp("completed_at", { withTimezone: true }),
sortOrder: integer("sort_order").notNull().default(0),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export type Todo = typeof todos.$inferSelect;
export type NewTodo = typeof todos.$inferInsert;
// ─── BetterAuth tables ───
export const users = pgTable("users", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").notNull().default(false),
image: text("image"),
role: text("role").notNull().default("user"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const sessions = pgTable("sessions", {
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id").notNull().references(() => users.id),
});
export const accounts = pgTable("accounts", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id").notNull().references(() => users.id),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at", { withTimezone: true }),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { withTimezone: true }),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const verifications = pgTable("verifications", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});