Initial commit: Backend with Bun.serve() + Drizzle ORM
- Bun 1.3.6 server setup - MariaDB schema (projects, agents, tasks) - Auto-migrations on startup - WebSocket support - Health check endpoint Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
33
src/db/client.ts
Normal file
33
src/db/client.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Database Client - Drizzle ORM + MySQL2
|
||||
*/
|
||||
|
||||
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 || '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',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
})
|
||||
|
||||
export const db = drizzle(pool, { schema, mode: 'default' })
|
||||
|
||||
// Test connection
|
||||
export async function testConnection() {
|
||||
try {
|
||||
const connection = await pool.getConnection()
|
||||
console.log('✅ Database connected successfully')
|
||||
connection.release()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('❌ Database connection failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
39
src/db/migrate.ts
Normal file
39
src/db/migrate.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Database Migrations Runner
|
||||
* Runs automatically on app startup
|
||||
*/
|
||||
|
||||
import { drizzle } from 'drizzle-orm/mysql2'
|
||||
import { migrate } from 'drizzle-orm/mysql2/migrator'
|
||||
import mysql from 'mysql2/promise'
|
||||
|
||||
export async function runMigrations() {
|
||||
console.log('🔄 Running database migrations...')
|
||||
|
||||
const connection = await mysql.createConnection({
|
||||
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,
|
||||
})
|
||||
|
||||
const db = drizzle(connection)
|
||||
|
||||
try {
|
||||
await migrate(db, { migrationsFolder: './drizzle/migrations' })
|
||||
console.log('✅ Migrations completed successfully')
|
||||
await connection.end()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error)
|
||||
await connection.end()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (import.meta.main) {
|
||||
await runMigrations()
|
||||
process.exit(0)
|
||||
}
|
||||
147
src/db/schema.ts
Normal file
147
src/db/schema.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Database Schema - Drizzle ORM
|
||||
* MariaDB 11.4 LTS
|
||||
*/
|
||||
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
mysqlTable,
|
||||
varchar,
|
||||
text,
|
||||
timestamp,
|
||||
json,
|
||||
int,
|
||||
mysqlEnum,
|
||||
boolean,
|
||||
bigint,
|
||||
index,
|
||||
} 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'),
|
||||
|
||||
// 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),
|
||||
}))
|
||||
|
||||
// ============================================
|
||||
// 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'),
|
||||
|
||||
// Status
|
||||
status: mysqlEnum('status', ['idle', 'busy', 'error', 'offline']).default('idle'),
|
||||
currentTaskId: varchar('current_task_id', { length: 36 }),
|
||||
|
||||
// Metrics
|
||||
tasksCompleted: int('tasks_completed').default(0),
|
||||
lastHeartbeat: timestamp('last_heartbeat'),
|
||||
|
||||
// Timestamps
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||
}, (table) => ({
|
||||
statusIdx: index('idx_status').on(table.status),
|
||||
}))
|
||||
|
||||
// ============================================
|
||||
// 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',
|
||||
]).default('backlog'),
|
||||
|
||||
// Assignment
|
||||
assignedAgentId: varchar('assigned_agent_id', { length: 36 }).references(() => agents.id, { onDelete: 'set null' }),
|
||||
|
||||
// Git
|
||||
branchName: varchar('branch_name', { length: 255 }),
|
||||
prUrl: varchar('pr_url', { length: 512 }),
|
||||
|
||||
// Preview
|
||||
previewUrl: varchar('preview_url', { length: 512 }),
|
||||
|
||||
// Timestamps
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||
}, (table) => ({
|
||||
projectStateIdx: index('idx_project_state').on(table.projectId, table.state),
|
||||
}))
|
||||
|
||||
// ============================================
|
||||
// RELATIONS
|
||||
// ============================================
|
||||
|
||||
export const projectsRelations = relations(projects, ({ many }) => ({
|
||||
tasks: many(tasks),
|
||||
}))
|
||||
|
||||
export const tasksRelations = relations(tasks, ({ one }) => ({
|
||||
project: one(projects, {
|
||||
fields: [tasks.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
assignedAgent: one(agents, {
|
||||
fields: [tasks.assignedAgentId],
|
||||
references: [agents.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export const agentsRelations = relations(agents, ({ one }) => ({
|
||||
currentTask: one(tasks, {
|
||||
fields: [agents.currentTaskId],
|
||||
references: [tasks.id],
|
||||
}),
|
||||
}))
|
||||
84
src/index.ts
Normal file
84
src/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* AiWorker Backend - Main Entry Point
|
||||
* Using Bun.serve() native API
|
||||
*/
|
||||
|
||||
import { runMigrations } from './db/migrate'
|
||||
import { testConnection } from './db/client'
|
||||
|
||||
console.log('🚀 Starting AiWorker Backend...')
|
||||
console.log(`Bun version: ${Bun.version}`)
|
||||
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`)
|
||||
|
||||
// Run migrations on startup
|
||||
await runMigrations()
|
||||
|
||||
// Test database connection
|
||||
await testConnection()
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
// Health check route
|
||||
function handleHealthCheck() {
|
||||
return Response.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
bun: Bun.version,
|
||||
})
|
||||
}
|
||||
|
||||
// Main server
|
||||
const server = Bun.serve({
|
||||
port: PORT,
|
||||
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
|
||||
// Health check
|
||||
if (url.pathname === '/api/health') {
|
||||
return handleHealthCheck()
|
||||
}
|
||||
|
||||
// API routes
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
return Response.json({
|
||||
message: 'AiWorker API',
|
||||
path: url.pathname,
|
||||
method: req.method,
|
||||
})
|
||||
}
|
||||
|
||||
// Root
|
||||
if (url.pathname === '/') {
|
||||
return new Response('AiWorker Backend - Running on Bun 🚀', {
|
||||
headers: { 'Content-Type': 'text/plain' }
|
||||
})
|
||||
}
|
||||
|
||||
// 404
|
||||
return new Response('Not Found', { status: 404 })
|
||||
},
|
||||
|
||||
// WebSocket support
|
||||
websocket: {
|
||||
open(ws) {
|
||||
console.log('WebSocket client connected')
|
||||
ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }))
|
||||
},
|
||||
message(ws, message) {
|
||||
console.log('WebSocket message:', message)
|
||||
// Echo back for now
|
||||
ws.send(message)
|
||||
},
|
||||
close(ws) {
|
||||
console.log('WebSocket client disconnected')
|
||||
},
|
||||
},
|
||||
|
||||
// Development mode
|
||||
development: process.env.NODE_ENV === 'development',
|
||||
})
|
||||
|
||||
console.log(`✅ Server listening on http://localhost:${server.port}`)
|
||||
console.log(`📊 Health check: http://localhost:${server.port}/api/health`)
|
||||
Reference in New Issue
Block a user