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 } from './domains/finance'; import { 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; db?: DatabaseConnection; } // ── Adding a new domain ─────────────────────────────────────────────── // 1. Create: server/domains// directory structure // 2. Move controllers, services, types to the domain // 3. Create barrel: server/domains//index.ts // 4. Import from domain and register controller below // ─────────────────────────────────────────────────────────────────────────── export async function buildApp({ logger = true, db: injectedDb }: 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 — use injected db (for tests) or create real one const db = injectedDb ?? (() => { const rawDb = createDb(); const audit = new QueryAudit(); return 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; }