Add database package with Drizzle schema

Postgres schema via Drizzle ORM for agents, goals, issues, projects,
and activity log tables. Includes migration runner, seed script,
and Drizzle config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-16 13:31:52 -06:00
parent b62fa4ad64
commit 948e8e8c94
14 changed files with 214 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/schema/index.ts",
out: "./src/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});

27
packages/db/package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "@paperclip/db",
"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"generate": "drizzle-kit generate",
"migrate": "tsx src/migrate.ts",
"seed": "tsx src/seed.ts"
},
"dependencies": {
"@paperclip/shared": "workspace:*",
"drizzle-orm": "^0.38.4",
"postgres": "^3.4.5"
},
"devDependencies": {
"drizzle-kit": "^0.30.4",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"vitest": "^3.0.5"
}
}

10
packages/db/src/client.ts Normal file
View File

@@ -0,0 +1,10 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema/index.js";
export function createDb(url: string) {
const sql = postgres(url);
return drizzle(sql, { schema });
}
export type Db = ReturnType<typeof createDb>;

2
packages/db/src/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { createDb, type Db } from "./client.js";
export * from "./schema/index.js";

View File

@@ -0,0 +1,13 @@
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
const url = process.env.DATABASE_URL;
if (!url) throw new Error("DATABASE_URL is required");
const sql = postgres(url, { max: 1 });
const db = drizzle(sql);
await migrate(db, { migrationsFolder: new URL("./migrations", import.meta.url).pathname });
await sql.end();
console.log("Migrations complete");

View File

@@ -0,0 +1,12 @@
import { pgTable, uuid, text, timestamp, jsonb } from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
export const activityLog = pgTable("activity_log", {
id: uuid("id").primaryKey().defaultRandom(),
action: text("action").notNull(),
entityType: text("entity_type").notNull(),
entityId: uuid("entity_id").notNull(),
agentId: uuid("agent_id").references(() => agents.id),
details: jsonb("details"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});

View File

@@ -0,0 +1,15 @@
import { type AnyPgColumn, pgTable, uuid, text, integer, timestamp, jsonb } from "drizzle-orm/pg-core";
export const agents = pgTable("agents", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
role: text("role").notNull().default("general"),
status: text("status").notNull().default("idle"),
budgetCents: integer("budget_cents").notNull().default(0),
spentCents: integer("spent_cents").notNull().default(0),
lastHeartbeat: timestamp("last_heartbeat", { withTimezone: true }),
reportsTo: uuid("reports_to").references((): AnyPgColumn => agents.id),
metadata: jsonb("metadata"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});

View File

@@ -0,0 +1,13 @@
import { type AnyPgColumn, pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
export const goals = pgTable("goals", {
id: uuid("id").primaryKey().defaultRandom(),
title: text("title").notNull(),
description: text("description"),
level: text("level").notNull().default("task"),
parentId: uuid("parent_id").references((): AnyPgColumn => goals.id),
ownerId: uuid("owner_id").references(() => agents.id),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});

View File

@@ -0,0 +1,5 @@
export { agents } from "./agents.js";
export { projects } from "./projects.js";
export { goals } from "./goals.js";
export { issues } from "./issues.js";
export { activityLog } from "./activity_log.js";

View File

@@ -0,0 +1,17 @@
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
import { projects } from "./projects.js";
import { goals } from "./goals.js";
export const issues = pgTable("issues", {
id: uuid("id").primaryKey().defaultRandom(),
title: text("title").notNull(),
description: text("description"),
status: text("status").notNull().default("backlog"),
priority: text("priority").notNull().default("medium"),
projectId: uuid("project_id").references(() => projects.id),
assigneeId: uuid("assignee_id").references(() => agents.id),
goalId: uuid("goal_id").references(() => goals.id),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});

View File

@@ -0,0 +1,9 @@
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
export const projects = pgTable("projects", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});

66
packages/db/src/seed.ts Normal file
View File

@@ -0,0 +1,66 @@
import { eq, inArray } from "drizzle-orm";
import { createDb } from "./client.js";
import { agents, projects, issues, goals } from "./schema/index.js";
const url = process.env.DATABASE_URL;
if (!url) throw new Error("DATABASE_URL is required");
const db = createDb(url);
console.log("Seeding database...");
const [ceo, engineer, researcher] = await db
.insert(agents)
.values([
{ name: "CEO Agent", role: "ceo", status: "active" },
{ name: "Engineer Agent", role: "engineer", status: "idle" },
{ name: "Researcher Agent", role: "researcher", status: "idle" },
])
.returning();
// Wire up reporting hierarchy: engineer and researcher report to CEO
await db
.update(agents)
.set({ reportsTo: ceo!.id })
.where(inArray(agents.id, [engineer!.id, researcher!.id]));
const [project] = await db
.insert(projects)
.values([{ name: "Paperclip MVP", description: "Build the initial paperclip management platform" }])
.returning();
const [goal] = await db
.insert(goals)
.values([
{
title: "Launch MVP",
description: "Ship the minimum viable product",
level: "milestone",
ownerId: ceo!.id,
},
])
.returning();
await db.insert(issues).values([
{
title: "Set up database schema",
description: "Create initial Drizzle schema with all core tables",
status: "done",
priority: "high",
projectId: project!.id,
assigneeId: engineer!.id,
goalId: goal!.id,
},
{
title: "Implement agent heartbeat",
description: "Add periodic heartbeat mechanism for agent health monitoring",
status: "in_progress",
priority: "medium",
projectId: project!.id,
assigneeId: engineer!.id,
goalId: goal!.id,
},
]);
console.log("Seed complete");
process.exit(0);

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
},
});