# Database Schema con Drizzle ORM ## Schema Definitions ```typescript // 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>(), 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(), mcpPermissions: json('mcp_permissions').$type>(), // 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(), 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().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>(), // 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```bash # 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 ``` ```typescript // 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) ```