75 lines
3.6 KiB
TypeScript
75 lines
3.6 KiB
TypeScript
import Fastify, { type FastifyRequest, type FastifyReply } from 'fastify';
|
|
import cors from '@fastify/cors';
|
|
import rateLimit from '@fastify/rate-limit';
|
|
import { ScreenerController } from './controllers/screener.controller';
|
|
import { FinanceController } from './controllers/finance.controller';
|
|
import { CallsController } from './controllers/calls.controller';
|
|
import { AnalyzeController } from './controllers/analyze.controller';
|
|
import { ScreenerEngine } from './services/ScreenerEngine';
|
|
import { BenchmarkProvider } from './services/BenchmarkProvider';
|
|
import { PortfolioAdvisor } from './services/PortfolioAdvisor';
|
|
import { CalendarService } from './services/CalendarService';
|
|
import { LLMAnalyst } from './services/LLMAnalyst';
|
|
import { CatalystAnalyst } from './services/CatalystAnalyst';
|
|
import { YahooFinanceClient } from './clients/YahooFinanceClient';
|
|
import { MarketCallRepository } from './repositories/MarketCallRepository';
|
|
import { PortfolioRepository } from './repositories/PortfolioRepository';
|
|
import { createDb } from './db/index';
|
|
import { noopLogger } from './utils/logger';
|
|
|
|
interface BuildAppOptions {
|
|
logger?: boolean;
|
|
}
|
|
|
|
// ── Adding a new domain ───────────────────────────────────────────────────
|
|
// 1. server/types/<domain>.model.ts — define request/response shapes
|
|
// 2. server/services/<Domain>.ts — business logic
|
|
// 3. server/controllers/<domain>.controller.ts — HTTP wiring (class + register)
|
|
// 4. Register: new <Domain>Controller(...).register(app) ← add 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' });
|
|
}
|
|
});
|
|
}
|
|
|
|
const db = createDb();
|
|
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 catalyst = new CatalystAnalyst({ logger: noopLogger });
|
|
|
|
new ScreenerController(engine).register(app);
|
|
new FinanceController(engine, new PortfolioRepository(db), advisor).register(app);
|
|
new CallsController(new MarketCallRepository(db), engine, calSvc).register(app);
|
|
new AnalyzeController(catalyst, llm).register(app);
|
|
|
|
app.get('/health', async () => ({ status: 'ok' }));
|
|
|
|
return app;
|
|
}
|