Files
market_screener/server/app.ts
T
2026-06-06 22:55:43 -04:00

91 lines
3.5 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 } 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/<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, 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;
}