137 lines
4.3 KiB
Markdown
137 lines
4.3 KiB
Markdown
# Phase 2: Shared Infrastructure
|
|
|
|
## Config.ts
|
|
Usa Zod para validar TODAS las env vars al arranque. Si falla → crash inmediato con mensaje claro.
|
|
```typescript
|
|
import { z } from 'zod';
|
|
import dotenv from 'dotenv';
|
|
dotenv.config();
|
|
|
|
const configSchema = z.object({
|
|
port: z.coerce.number().default(3001),
|
|
host: z.string().default('0.0.0.0'),
|
|
nodeEnv: z.enum(['development', 'production', 'test']).default('development'),
|
|
db: z.object({
|
|
driver: z.enum(['sqlite', 'postgres']).default('sqlite'),
|
|
path: z.string().default('./data/abe.db'),
|
|
url: z.string().optional(),
|
|
}),
|
|
auth: z.object({
|
|
secret: z.string().min(16).default('abe-dev-secret-change-in-prod'),
|
|
sessionMaxAge: z.coerce.number().default(86400),
|
|
}),
|
|
storage: z.object({
|
|
driver: z.enum(['local', 's3']).default('local'),
|
|
path: z.string().default('./data/storage'),
|
|
}),
|
|
cors: z.object({ origin: z.string().default('http://localhost:5173') }),
|
|
log: z.object({ level: z.enum(['debug','info','warn','error']).default('info') }),
|
|
api: z.object({
|
|
key: z.string().default('abe-dev-key-123'),
|
|
rateLimitWindowMs: z.coerce.number().default(900000),
|
|
rateLimitMax: z.coerce.number().default(100),
|
|
}),
|
|
ai: z.object({
|
|
provider: z.enum(['claude','openai','ollama','none']).default('none'),
|
|
apiKey: z.string().default(''),
|
|
autoEnrich: z.coerce.boolean().default(false),
|
|
minSeverity: z.enum(['low','medium','high','critical']).default('high'),
|
|
}),
|
|
jobs: z.object({
|
|
maxConcurrentSessions: z.coerce.number().default(3),
|
|
pollIntervalMs: z.coerce.number().default(1000),
|
|
}),
|
|
license: z.object({ key: z.string().default('') }),
|
|
});
|
|
|
|
export type AppConfig = z.infer<typeof configSchema>;
|
|
|
|
export function loadConfig(): AppConfig {
|
|
// Map env vars to schema shape, parse
|
|
}
|
|
```
|
|
|
|
## Logger.ts
|
|
```typescript
|
|
import pino from 'pino';
|
|
|
|
export function createLogger(config: { level: string; nodeEnv: string }): pino.Logger {
|
|
return pino({
|
|
level: config.level,
|
|
transport: config.nodeEnv === 'development'
|
|
? { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss' } }
|
|
: undefined,
|
|
});
|
|
}
|
|
export type Logger = pino.Logger;
|
|
```
|
|
|
|
## DatabaseConnection.ts
|
|
```typescript
|
|
import { Kysely, SqliteDialect } from 'kysely';
|
|
import SQLite from 'better-sqlite3';
|
|
|
|
// Define Database interface con todas las tablas
|
|
export interface Database {
|
|
sessions: SessionTable;
|
|
states: StateTable;
|
|
actions: ActionTable;
|
|
anomalies: AnomalyTable;
|
|
// ... más tablas se añaden en fases posteriores
|
|
}
|
|
|
|
export function createDatabase(config: { driver: string; path: string; url?: string }): Kysely<Database> {
|
|
if (config.driver === 'postgres') {
|
|
// Import dinámico de pg para no requerir en SQLite
|
|
const { Pool } = require('pg');
|
|
const { PostgresDialect } = require('kysely');
|
|
return new Kysely<Database>({
|
|
dialect: new PostgresDialect({ pool: new Pool({ connectionString: config.url }) }),
|
|
});
|
|
}
|
|
|
|
// Crear directorio data/ si no existe
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
fs.mkdirSync(path.dirname(config.path), { recursive: true });
|
|
|
|
return new Kysely<Database>({
|
|
dialect: new SqliteDialect({ database: new SQLite(config.path) }),
|
|
});
|
|
}
|
|
```
|
|
|
|
## InProcessEventBus.ts
|
|
```typescript
|
|
import { EventEmitter } from 'events';
|
|
// Implements EventBus interface from shared/application
|
|
// Logging de cada evento publicado
|
|
// Catch errors en handlers (log pero no crash)
|
|
// setMaxListeners(50)
|
|
```
|
|
|
|
## StorageProvider.ts
|
|
```typescript
|
|
export interface IStorageProvider {
|
|
save(relativePath: string, data: Buffer): Promise<string>;
|
|
get(relativePath: string): Promise<Buffer | null>;
|
|
delete(relativePath: string): Promise<void>;
|
|
exists(relativePath: string): Promise<boolean>;
|
|
}
|
|
|
|
// LocalStorageProvider: usa fs.promises, base path = config.storage.path
|
|
// Crea directorios automáticamente con mkdir recursive
|
|
```
|
|
|
|
## Migración 001
|
|
Crea las tablas que ya existen en el schema actual (sessions, states, actions, anomalies, notifications).
|
|
Usar `CREATE TABLE IF NOT EXISTS` para idempotencia.
|
|
Los tipos de columna deben coincidir con lo que ya tiene better-sqlite3.
|
|
|
|
## IMPORTANTE
|
|
- Config DEBE fallar rápido si hay env vars inválidas
|
|
- Logger NUNCA debe usar console.log
|
|
- Database factory NUNCA importa pg a menos que driver sea postgres
|
|
- EventBus handlers que fallan se loguean pero NO crashean el bus
|
|
|