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>
This commit is contained in:
462
docs/02-backend/database-schema.md
Normal file
462
docs/02-backend/database-schema.md
Normal file
@@ -0,0 +1,462 @@
|
||||
# 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<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
|
||||
|
||||
```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)
|
||||
```
|
||||
Reference in New Issue
Block a user