Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
374 lines
14 KiB
TypeScript
374 lines
14 KiB
TypeScript
/**
|
|
* Unified schema file — all tables defined here.
|
|
* drizzle-kit reads this file directly (avoids CJS/ESM extension issues).
|
|
*/
|
|
|
|
import {
|
|
pgTable,
|
|
text,
|
|
timestamp,
|
|
boolean,
|
|
uuid,
|
|
jsonb,
|
|
index,
|
|
real,
|
|
integer,
|
|
customType,
|
|
} from 'drizzle-orm/pg-core';
|
|
|
|
// ─── Auth (BetterAuth-compatible) ────────────────────────────────────────────
|
|
|
|
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('member'),
|
|
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(),
|
|
ipAddress: text('ip_address'),
|
|
userAgent: text('user_agent'),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: 'cascade' }),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
});
|
|
|
|
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, { onDelete: 'cascade' }),
|
|
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(),
|
|
});
|
|
|
|
// ─── Brain ───────────────────────────────────────────────────────────────────
|
|
// Declared before Chat because conversations references projects.
|
|
|
|
export const projects = pgTable('projects', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
status: text('status', { enum: ['active', 'paused', 'completed', 'archived'] })
|
|
.notNull()
|
|
.default('active'),
|
|
ownerId: text('owner_id').references(() => users.id, { onDelete: 'set null' }),
|
|
metadata: jsonb('metadata'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
});
|
|
|
|
export const missions = pgTable(
|
|
'missions',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
status: text('status', { enum: ['planning', 'active', 'paused', 'completed', 'failed'] })
|
|
.notNull()
|
|
.default('planning'),
|
|
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
|
|
metadata: jsonb('metadata'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [index('missions_project_id_idx').on(t.projectId)],
|
|
);
|
|
|
|
export const tasks = pgTable(
|
|
'tasks',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
status: text('status', {
|
|
enum: ['not-started', 'in-progress', 'blocked', 'done', 'cancelled'],
|
|
})
|
|
.notNull()
|
|
.default('not-started'),
|
|
priority: text('priority', { enum: ['critical', 'high', 'medium', 'low'] })
|
|
.notNull()
|
|
.default('medium'),
|
|
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
|
|
missionId: uuid('mission_id').references(() => missions.id, { onDelete: 'set null' }),
|
|
assignee: text('assignee'),
|
|
tags: jsonb('tags').$type<string[]>(),
|
|
dueDate: timestamp('due_date', { withTimezone: true }),
|
|
metadata: jsonb('metadata'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
index('tasks_project_id_idx').on(t.projectId),
|
|
index('tasks_mission_id_idx').on(t.missionId),
|
|
index('tasks_status_idx').on(t.status),
|
|
],
|
|
);
|
|
|
|
export const events = pgTable(
|
|
'events',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
type: text('type').notNull(),
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
date: timestamp('date', { withTimezone: true }).notNull().defaultNow(),
|
|
metadata: jsonb('metadata'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [index('events_type_idx').on(t.type), index('events_date_idx').on(t.date)],
|
|
);
|
|
|
|
export const agents = pgTable('agents', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
provider: text('provider').notNull(),
|
|
model: text('model').notNull(),
|
|
status: text('status', { enum: ['idle', 'active', 'error', 'offline'] })
|
|
.notNull()
|
|
.default('idle'),
|
|
config: jsonb('config'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
});
|
|
|
|
export const tickets = pgTable(
|
|
'tickets',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
status: text('status', { enum: ['open', 'in-progress', 'resolved', 'closed'] })
|
|
.notNull()
|
|
.default('open'),
|
|
priority: text('priority', { enum: ['critical', 'high', 'medium', 'low'] })
|
|
.notNull()
|
|
.default('medium'),
|
|
source: text('source'),
|
|
metadata: jsonb('metadata'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [index('tickets_status_idx').on(t.status)],
|
|
);
|
|
|
|
export const appreciations = pgTable('appreciations', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
fromUser: text('from_user'),
|
|
toUser: text('to_user'),
|
|
message: text('message').notNull(),
|
|
metadata: jsonb('metadata'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
});
|
|
|
|
// ─── Chat ────────────────────────────────────────────────────────────────────
|
|
|
|
export const conversations = pgTable(
|
|
'conversations',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
title: text('title'),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: 'cascade' }),
|
|
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
index('conversations_user_id_idx').on(t.userId),
|
|
index('conversations_project_id_idx').on(t.projectId),
|
|
],
|
|
);
|
|
|
|
export const messages = pgTable(
|
|
'messages',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
conversationId: uuid('conversation_id')
|
|
.notNull()
|
|
.references(() => conversations.id, { onDelete: 'cascade' }),
|
|
role: text('role', { enum: ['user', 'assistant', 'system'] }).notNull(),
|
|
content: text('content').notNull(),
|
|
metadata: jsonb('metadata'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [index('messages_conversation_id_idx').on(t.conversationId)],
|
|
);
|
|
|
|
// ─── pgvector custom type ───────────────────────────────────────────────────
|
|
|
|
const vector = customType<{ data: number[]; driverParam: string; config: { dimensions: number } }>({
|
|
dataType(config) {
|
|
return `vector(${config?.dimensions ?? 1536})`;
|
|
},
|
|
fromDriver(value: unknown): number[] {
|
|
const str = value as string;
|
|
return str
|
|
.slice(1, -1)
|
|
.split(',')
|
|
.map((v) => Number(v));
|
|
},
|
|
toDriver(value: number[]): string {
|
|
return `[${value.join(',')}]`;
|
|
},
|
|
});
|
|
|
|
// ─── Memory ─────────────────────────────────────────────────────────────────
|
|
|
|
export const preferences = pgTable(
|
|
'preferences',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: 'cascade' }),
|
|
key: text('key').notNull(),
|
|
value: jsonb('value').notNull(),
|
|
category: text('category', {
|
|
enum: ['communication', 'coding', 'workflow', 'appearance', 'general'],
|
|
})
|
|
.notNull()
|
|
.default('general'),
|
|
source: text('source'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
index('preferences_user_id_idx').on(t.userId),
|
|
index('preferences_user_key_idx').on(t.userId, t.key),
|
|
],
|
|
);
|
|
|
|
export const insights = pgTable(
|
|
'insights',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: 'cascade' }),
|
|
content: text('content').notNull(),
|
|
embedding: vector('embedding', { dimensions: 1536 }),
|
|
source: text('source', {
|
|
enum: ['agent', 'user', 'summarization', 'system'],
|
|
})
|
|
.notNull()
|
|
.default('agent'),
|
|
category: text('category', {
|
|
enum: ['decision', 'learning', 'preference', 'fact', 'pattern', 'general'],
|
|
})
|
|
.notNull()
|
|
.default('general'),
|
|
relevanceScore: real('relevance_score').notNull().default(1.0),
|
|
metadata: jsonb('metadata'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
decayedAt: timestamp('decayed_at', { withTimezone: true }),
|
|
},
|
|
(t) => [
|
|
index('insights_user_id_idx').on(t.userId),
|
|
index('insights_category_idx').on(t.category),
|
|
index('insights_relevance_idx').on(t.relevanceScore),
|
|
],
|
|
);
|
|
|
|
// ─── Agent Logs ─────────────────────────────────────────────────────────────
|
|
|
|
export const agentLogs = pgTable(
|
|
'agent_logs',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
sessionId: text('session_id').notNull(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
|
|
level: text('level', { enum: ['debug', 'info', 'warn', 'error'] })
|
|
.notNull()
|
|
.default('info'),
|
|
category: text('category', {
|
|
enum: ['decision', 'tool_use', 'learning', 'error', 'general'],
|
|
})
|
|
.notNull()
|
|
.default('general'),
|
|
content: text('content').notNull(),
|
|
metadata: jsonb('metadata'),
|
|
tier: text('tier', { enum: ['hot', 'warm', 'cold'] })
|
|
.notNull()
|
|
.default('hot'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
summarizedAt: timestamp('summarized_at', { withTimezone: true }),
|
|
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
|
},
|
|
(t) => [
|
|
index('agent_logs_session_id_idx').on(t.sessionId),
|
|
index('agent_logs_user_id_idx').on(t.userId),
|
|
index('agent_logs_tier_idx').on(t.tier),
|
|
index('agent_logs_created_at_idx').on(t.createdAt),
|
|
],
|
|
);
|
|
|
|
// ─── Skills ─────────────────────────────────────────────────────────────────
|
|
|
|
export const skills = pgTable(
|
|
'skills',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull().unique(),
|
|
description: text('description'),
|
|
version: text('version'),
|
|
source: text('source', { enum: ['builtin', 'community', 'custom'] })
|
|
.notNull()
|
|
.default('custom'),
|
|
config: jsonb('config'),
|
|
enabled: boolean('enabled').notNull().default(true),
|
|
installedBy: text('installed_by').references(() => users.id, { onDelete: 'set null' }),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [index('skills_enabled_idx').on(t.enabled)],
|
|
);
|
|
|
|
// ─── Summarization Jobs ─────────────────────────────────────────────────────
|
|
|
|
export const summarizationJobs = pgTable(
|
|
'summarization_jobs',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
status: text('status', { enum: ['pending', 'running', 'completed', 'failed'] })
|
|
.notNull()
|
|
.default('pending'),
|
|
logsProcessed: integer('logs_processed').notNull().default(0),
|
|
insightsCreated: integer('insights_created').notNull().default(0),
|
|
errorMessage: text('error_message'),
|
|
startedAt: timestamp('started_at', { withTimezone: true }),
|
|
completedAt: timestamp('completed_at', { withTimezone: true }),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [index('summarization_jobs_status_idx').on(t.status)],
|
|
);
|