- CLAUDE.md for AI agents to understand the codebase - GITEA-GUIDE.md centralizes all Gitea operations (API, Registry, Auth) - DEVELOPMENT-WORKFLOW.md explains complete dev process - ROADMAP.md, NEXT-SESSION.md for planning - QUICK-REFERENCE.md, TROUBLESHOOTING.md for daily use - 40+ detailed docs in /docs folder - Backend as submodule from Gitea Everything documented for autonomous operation. Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
14 KiB
14 KiB
Database Schema con Drizzle ORM
Schema Definitions
// db/schema.ts
import { relations } from 'drizzle-orm'
import {
mysqlTable,
varchar,
text,
timestamp,
json,
int,
mysqlEnum,
boolean,
bigint,
index,
unique,
} from 'drizzle-orm/mysql-core'
// ============================================
// PROJECTS TABLE
// ============================================
export const projects = mysqlTable('projects', {
id: varchar('id', { length: 36 }).primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
description: text('description'),
// Gitea
giteaRepoId: int('gitea_repo_id'),
giteaRepoUrl: varchar('gitea_repo_url', { length: 512 }),
giteaOwner: varchar('gitea_owner', { length: 100 }),
giteaRepoName: varchar('gitea_repo_name', { length: 100 }),
defaultBranch: varchar('default_branch', { length: 100 }).default('main'),
// K8s
k8sNamespace: varchar('k8s_namespace', { length: 63 }).notNull().unique(),
// Infrastructure
dockerImage: varchar('docker_image', { length: 512 }),
envVars: json('env_vars').$type<Record<string, string>>(),
replicas: int('replicas').default(1),
cpuLimit: varchar('cpu_limit', { length: 20 }).default('500m'),
memoryLimit: varchar('memory_limit', { length: 20 }).default('512Mi'),
// MCP
mcpTools: json('mcp_tools').$type<string[]>(),
mcpPermissions: json('mcp_permissions').$type<Record<string, any>>(),
// Status
status: mysqlEnum('status', ['active', 'paused', 'archived']).default('active'),
// Timestamps
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
}, (table) => ({
statusIdx: index('idx_status').on(table.status),
k8sNamespaceIdx: index('idx_k8s_namespace').on(table.k8sNamespace),
}))
// ============================================
// AGENTS TABLE
// ============================================
export const agents = mysqlTable('agents', {
id: varchar('id', { length: 36 }).primaryKey(),
// K8s
podName: varchar('pod_name', { length: 253 }).notNull().unique(),
k8sNamespace: varchar('k8s_namespace', { length: 63 }).default('agents'),
nodeName: varchar('node_name', { length: 253 }),
// Status
status: mysqlEnum('status', ['idle', 'busy', 'error', 'offline', 'initializing']).default('initializing'),
currentTaskId: varchar('current_task_id', { length: 36 }),
// Capabilities
capabilities: json('capabilities').$type<string[]>(),
maxConcurrentTasks: int('max_concurrent_tasks').default(1),
// Health
lastHeartbeat: timestamp('last_heartbeat'),
errorMessage: text('error_message'),
restartsCount: int('restarts_count').default(0),
// Metrics
tasksCompleted: int('tasks_completed').default(0),
totalRuntimeMinutes: int('total_runtime_minutes').default(0),
// Timestamps
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
}, (table) => ({
statusIdx: index('idx_status').on(table.status),
podNameIdx: index('idx_pod_name').on(table.podName),
lastHeartbeatIdx: index('idx_last_heartbeat').on(table.lastHeartbeat),
}))
// ============================================
// TASKS TABLE
// ============================================
export const tasks = mysqlTable('tasks', {
id: varchar('id', { length: 36 }).primaryKey(),
projectId: varchar('project_id', { length: 36 }).notNull().references(() => projects.id, { onDelete: 'cascade' }),
// Task info
title: varchar('title', { length: 255 }).notNull(),
description: text('description'),
priority: mysqlEnum('priority', ['low', 'medium', 'high', 'urgent']).default('medium'),
// State
state: mysqlEnum('state', [
'backlog',
'in_progress',
'needs_input',
'ready_to_test',
'approved',
'staging',
'production',
'cancelled'
]).default('backlog'),
// Assignment
assignedAgentId: varchar('assigned_agent_id', { length: 36 }).references(() => agents.id, { onDelete: 'set null' }),
assignedAt: timestamp('assigned_at'),
// Git
branchName: varchar('branch_name', { length: 255 }),
prNumber: int('pr_number'),
prUrl: varchar('pr_url', { length: 512 }),
// Preview
previewNamespace: varchar('preview_namespace', { length: 63 }),
previewUrl: varchar('preview_url', { length: 512 }),
previewDeployedAt: timestamp('preview_deployed_at'),
// Metadata
estimatedComplexity: mysqlEnum('estimated_complexity', ['trivial', 'simple', 'medium', 'complex']).default('medium'),
actualDurationMinutes: int('actual_duration_minutes'),
// Timestamps
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
startedAt: timestamp('started_at'),
completedAt: timestamp('completed_at'),
deployedStagingAt: timestamp('deployed_staging_at'),
deployedProductionAt: timestamp('deployed_production_at'),
}, (table) => ({
projectStateIdx: index('idx_project_state').on(table.projectId, table.state, table.createdAt),
stateIdx: index('idx_state').on(table.state),
assignedAgentIdx: index('idx_assigned_agent').on(table.assignedAgentId),
createdAtIdx: index('idx_created_at').on(table.createdAt),
}))
// ============================================
// TASK QUESTIONS TABLE
// ============================================
export const taskQuestions = mysqlTable('task_questions', {
id: varchar('id', { length: 36 }).primaryKey(),
taskId: varchar('task_id', { length: 36 }).notNull().references(() => tasks.id, { onDelete: 'cascade' }),
// Question
question: text('question').notNull(),
context: text('context'),
askedAt: timestamp('asked_at').defaultNow(),
// Response
response: text('response'),
respondedAt: timestamp('responded_at'),
respondedBy: varchar('responded_by', { length: 36 }),
// Status
status: mysqlEnum('status', ['pending', 'answered', 'skipped']).default('pending'),
}, (table) => ({
taskStatusIdx: index('idx_task_status').on(table.taskId, table.status),
statusIdx: index('idx_status').on(table.status),
}))
// ============================================
// TASK GROUPS TABLE
// ============================================
export const taskGroups = mysqlTable('task_groups', {
id: varchar('id', { length: 36 }).primaryKey(),
projectId: varchar('project_id', { length: 36 }).notNull().references(() => projects.id, { onDelete: 'cascade' }),
// Grouping
taskIds: json('task_ids').$type<string[]>().notNull(),
// Staging
stagingBranch: varchar('staging_branch', { length: 255 }),
stagingPrNumber: int('staging_pr_number'),
stagingPrUrl: varchar('staging_pr_url', { length: 512 }),
stagingDeployedAt: timestamp('staging_deployed_at'),
// Production
productionDeployedAt: timestamp('production_deployed_at'),
productionRollbackAvailable: boolean('production_rollback_available').default(true),
// Status
status: mysqlEnum('status', ['pending', 'staging', 'production', 'rolled_back']).default('pending'),
// Metadata
createdBy: varchar('created_by', { length: 36 }),
notes: text('notes'),
// Timestamps
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
}, (table) => ({
projectStatusIdx: index('idx_project_status').on(table.projectId, table.status),
statusIdx: index('idx_status').on(table.status),
}))
// ============================================
// DEPLOYMENTS TABLE
// ============================================
export const deployments = mysqlTable('deployments', {
id: varchar('id', { length: 36 }).primaryKey(),
projectId: varchar('project_id', { length: 36 }).notNull().references(() => projects.id, { onDelete: 'cascade' }),
taskGroupId: varchar('task_group_id', { length: 36 }).references(() => taskGroups.id, { onDelete: 'set null' }),
// Deployment info
environment: mysqlEnum('environment', ['preview', 'staging', 'production']).notNull(),
deploymentType: mysqlEnum('deployment_type', ['manual', 'automatic', 'rollback']).default('manual'),
// Git
branch: varchar('branch', { length: 255 }),
commitHash: varchar('commit_hash', { length: 40 }),
// K8s
k8sNamespace: varchar('k8s_namespace', { length: 63 }),
k8sDeploymentName: varchar('k8s_deployment_name', { length: 253 }),
imageTag: varchar('image_tag', { length: 255 }),
// Status
status: mysqlEnum('status', ['pending', 'in_progress', 'completed', 'failed', 'rolled_back']).default('pending'),
// Results
url: varchar('url', { length: 512 }),
errorMessage: text('error_message'),
logs: text('logs'),
// Timing
startedAt: timestamp('started_at'),
completedAt: timestamp('completed_at'),
durationSeconds: int('duration_seconds'),
// Metadata
triggeredBy: varchar('triggered_by', { length: 36 }),
// Timestamps
createdAt: timestamp('created_at').defaultNow(),
}, (table) => ({
projectEnvIdx: index('idx_project_env').on(table.projectId, table.environment),
statusIdx: index('idx_status').on(table.status),
createdAtIdx: index('idx_created_at').on(table.createdAt),
}))
// ============================================
// AGENT LOGS TABLE
// ============================================
export const agentLogs = mysqlTable('agent_logs', {
id: bigint('id', { mode: 'number' }).autoincrement().primaryKey(),
agentId: varchar('agent_id', { length: 36 }).notNull().references(() => agents.id, { onDelete: 'cascade' }),
taskId: varchar('task_id', { length: 36 }).references(() => tasks.id, { onDelete: 'set null' }),
// Log entry
level: mysqlEnum('level', ['debug', 'info', 'warn', 'error']).default('info'),
message: text('message').notNull(),
metadata: json('metadata').$type<Record<string, any>>(),
// Timestamp
createdAt: timestamp('created_at').defaultNow(),
}, (table) => ({
agentCreatedIdx: index('idx_agent_created').on(table.agentId, table.createdAt),
taskCreatedIdx: index('idx_task_created').on(table.taskId, table.createdAt),
levelIdx: index('idx_level').on(table.level),
}))
// ============================================
// RELATIONS
// ============================================
export const projectsRelations = relations(projects, ({ many }) => ({
tasks: many(tasks),
taskGroups: many(taskGroups),
deployments: many(deployments),
}))
export const tasksRelations = relations(tasks, ({ one, many }) => ({
project: one(projects, {
fields: [tasks.projectId],
references: [projects.id],
}),
assignedAgent: one(agents, {
fields: [tasks.assignedAgentId],
references: [agents.id],
}),
questions: many(taskQuestions),
}))
export const agentsRelations = relations(agents, ({ one, many }) => ({
currentTask: one(tasks, {
fields: [agents.currentTaskId],
references: [tasks.id],
}),
logs: many(agentLogs),
}))
export const taskQuestionsRelations = relations(taskQuestions, ({ one }) => ({
task: one(tasks, {
fields: [taskQuestions.taskId],
references: [tasks.id],
}),
}))
export const taskGroupsRelations = relations(taskGroups, ({ one, many }) => ({
project: one(projects, {
fields: [taskGroups.projectId],
references: [projects.id],
}),
deployments: many(deployments),
}))
export const deploymentsRelations = relations(deployments, ({ one }) => ({
project: one(projects, {
fields: [deployments.projectId],
references: [projects.id],
}),
taskGroup: one(taskGroups, {
fields: [deployments.taskGroupId],
references: [taskGroups.id],
}),
}))
export const agentLogsRelations = relations(agentLogs, ({ one }) => ({
agent: one(agents, {
fields: [agentLogs.agentId],
references: [agents.id],
}),
task: one(tasks, {
fields: [agentLogs.taskId],
references: [tasks.id],
}),
}))
Drizzle Configuration
// drizzle.config.ts
import type { Config } from 'drizzle-kit'
export default {
schema: './src/db/schema.ts',
out: './drizzle/migrations',
driver: 'mysql2',
dbCredentials: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306'),
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'aiworker',
},
} satisfies Config
Database Client
// db/client.ts
import { drizzle } from 'drizzle-orm/mysql2'
import mysql from 'mysql2/promise'
import * as schema from './schema'
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '3306'),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
})
export const db = drizzle(pool, { schema, mode: 'default' })
Ejemplos de Queries
// Get all tasks for a project
const projectTasks = await db.query.tasks.findMany({
where: eq(tasks.projectId, projectId),
with: {
assignedAgent: true,
questions: {
where: eq(taskQuestions.status, 'pending')
}
},
orderBy: [desc(tasks.createdAt)]
})
// Get next available task
const nextTask = await db.query.tasks.findFirst({
where: eq(tasks.state, 'backlog'),
orderBy: [desc(tasks.priority), asc(tasks.createdAt)]
})
// Get idle agents
const idleAgents = await db.query.agents.findMany({
where: and(
eq(agents.status, 'idle'),
gt(agents.lastHeartbeat, new Date(Date.now() - 60000))
)
})
// Insert new task
const newTask = await db.insert(tasks).values({
id: crypto.randomUUID(),
projectId: projectId,
title: 'New task',
description: 'Task description',
state: 'backlog',
priority: 'medium',
})
Migrations
# Generate migration
bun run drizzle-kit generate:mysql
# Push changes directly (dev only)
bun run drizzle-kit push:mysql
# Run migrations
bun run scripts/migrate.ts
// scripts/migrate.ts
import { migrate } from 'drizzle-orm/mysql2/migrator'
import { db } from '../src/db/client'
async function runMigrations() {
await migrate(db, { migrationsFolder: './drizzle/migrations' })
console.log('✓ Migrations completed')
process.exit(0)
}
runMigrations().catch(console.error)