c388b6d83c
- Restructured server layer with 5 domains: shared, screener, portfolio, calls, finance - Migrated 58 TypeScript files to domain-driven structure - Updated CLAUDE.md with new architecture documentation - Added .gitignore rules for .md files (except CLAUDE.md) - Removed unused CatalystAnalyst import from app.ts - Fixed lint errors: removed unused imports, fixed regex escape, added console suppressions - Verified no sensitive data in git history - Server code compiles cleanly with TypeScript strict mode
85 lines
3.3 KiB
TypeScript
85 lines
3.3 KiB
TypeScript
import Fastify, { type FastifyRequest, type FastifyReply } from 'fastify';
|
|
import cors from '@fastify/cors';
|
|
import rateLimit from '@fastify/rate-limit';
|
|
|
|
// Domain imports
|
|
import { ScreenerController, ScreenerEngine, AnalyzeController } from './domains/screener';
|
|
import { FinanceController, PortfolioAdvisor } from './domains/portfolio';
|
|
import { CallsController, CalendarService } from './domains/calls';
|
|
|
|
// Shared infrastructure
|
|
import {
|
|
YahooFinanceClient,
|
|
BenchmarkProvider,
|
|
CatalystCache,
|
|
LLMAnalyst,
|
|
MarketCallRepository,
|
|
PortfolioRepository,
|
|
createDb,
|
|
DatabaseConnection,
|
|
QueryAudit,
|
|
noopLogger,
|
|
} from './domains/shared';
|
|
|
|
interface BuildAppOptions {
|
|
logger?: boolean;
|
|
}
|
|
|
|
// ── Adding a new domain ───────────────────────────────────────────────
|
|
// 1. Create: server/domains/<domain>/ directory structure
|
|
// 2. Move controllers, services, types to the domain
|
|
// 3. Create barrel: server/domains/<domain>/index.ts
|
|
// 4. Import from domain and register controller below
|
|
// ───────────────────────────────────────────────────────────────────────────
|
|
export async function buildApp({ logger = true }: BuildAppOptions = {}) {
|
|
const app = Fastify({ logger });
|
|
|
|
await app.register(cors, {
|
|
origin: process.env.CLIENT_ORIGIN ?? 'http://localhost:5173',
|
|
});
|
|
|
|
// ── Rate limiting — applied globally, tightest on expensive routes ───────
|
|
await app.register(rateLimit, {
|
|
global: false, // opt-in per route via config.rateLimit
|
|
max: 60,
|
|
timeWindow: '1 minute',
|
|
});
|
|
|
|
// ── API key auth — only enforced when API_KEY env var is set ─────────────
|
|
const API_KEY = process.env.API_KEY;
|
|
if (API_KEY) {
|
|
app.addHook('onRequest', async (req: FastifyRequest, reply: FastifyReply) => {
|
|
// Skip auth for health check and OPTIONS preflight
|
|
if (req.url === '/health' || req.method === 'OPTIONS') return;
|
|
const header = req.headers['authorization'] ?? '';
|
|
if (header !== `Bearer ${API_KEY}`) {
|
|
return reply.code(401).send({ error: 'Unauthorized' });
|
|
}
|
|
});
|
|
}
|
|
|
|
// Database setup
|
|
const rawDb = createDb();
|
|
const audit = new QueryAudit();
|
|
const db = new DatabaseConnection(rawDb, { audit, logSlowQueries: 100 });
|
|
|
|
// Services and clients
|
|
const yahoo = new YahooFinanceClient();
|
|
const benchmark = new BenchmarkProvider(yahoo, { logger: noopLogger });
|
|
const engine = new ScreenerEngine(yahoo, benchmark, { logger: noopLogger });
|
|
const advisor = new PortfolioAdvisor(yahoo);
|
|
const calSvc = new CalendarService(yahoo);
|
|
const llm = new LLMAnalyst({ logger: noopLogger });
|
|
const catalystCache = new CatalystCache({ logger: noopLogger }); // Singleton, cached for 15m
|
|
|
|
// Register controllers
|
|
new ScreenerController(engine, catalystCache).register(app);
|
|
new FinanceController(engine, new PortfolioRepository(db), advisor).register(app);
|
|
new CallsController(new MarketCallRepository(db), engine, calSvc).register(app);
|
|
new AnalyzeController(catalystCache, llm).register(app);
|
|
|
|
app.get('/health', async () => ({ status: 'ok' }));
|
|
|
|
return app;
|
|
}
|