Files
aiworker/docs/02-backend/database-schema.md
Hector Ros db71705842 Complete documentation for future sessions
- 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>
2026-01-20 00:37:19 +01:00

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)