phase-9: domain-driven architecture complete

- 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
This commit is contained in:
Kazuma
2026-06-06 13:21:24 -04:00
committed by Kazuma
parent fbd166b1b7
commit 2e7860637e
88 changed files with 3576 additions and 3493 deletions
@@ -0,0 +1,96 @@
import { randomUUID } from 'crypto';
import { DatabaseConnection } from '../db/index';
import { QueryBuilder } from '../utils/QueryBuilder';
import { sanitizeString, sanitizeDate } from '../utils/sanitizer';
import type { MarketCall, CreateCallInput, MarketCallRow } from '../types';
export class MarketCallRepository {
constructor(private readonly db: DatabaseConnection) {}
/**
* Get all market calls, newest first.
*/
list(): (MarketCall & { createdAt: string })[] {
const qb = new QueryBuilder('MARKET_CALLS_QUERIES.SELECT_ALL');
const rows = this.db.all<MarketCallRow>(qb);
return rows.map(MarketCallRepository.toCall);
}
/**
* Get a single market call by ID.
*/
get(id: string): (MarketCall & { createdAt: string }) | null {
const qb = new QueryBuilder('MARKET_CALLS_QUERIES.SELECT_BY_ID', [id]);
const row = this.db.get<MarketCallRow>(qb);
return row ? MarketCallRepository.toCall(row) : null;
}
/**
* Create a new market call with snapshot of current prices.
*/
create({
title,
quarter,
date,
thesis,
tickers,
snapshot,
}: CreateCallInput): MarketCall & { createdAt: string } {
// Sanitize inputs
const sanitizedTitle = sanitizeString(title, 'title', 255);
const sanitizedQuarter = sanitizeString(quarter, 'quarter', 10);
const sanitizedThesis = sanitizeString(thesis, 'thesis', 2000);
const sanitizedDate = date ? sanitizeDate(date, 'date') : new Date().toISOString().slice(0, 10);
const call = {
id: randomUUID(),
title: sanitizedTitle,
quarter: sanitizedQuarter,
date: sanitizedDate,
thesis: sanitizedThesis,
tickers: tickers ?? [],
snapshot: snapshot ?? {},
createdAt: new Date().toISOString(),
};
const qb = new QueryBuilder('MARKET_CALLS_QUERIES.INSERT', [
call.id,
call.title,
call.quarter,
call.date,
call.thesis,
JSON.stringify(call.tickers),
JSON.stringify(call.snapshot),
call.createdAt,
]);
this.db.run(qb);
return call as MarketCall & { createdAt: string };
}
/**
* Delete a market call by ID.
* Returns true if the call existed and was deleted, false otherwise.
*/
delete(id: string): boolean {
const qb = new QueryBuilder('MARKET_CALLS_QUERIES.DELETE_BY_ID', [id]);
const changes = this.db.run(qb);
return changes > 0;
}
/**
* Convert database row to domain object.
*/
private static toCall(row: MarketCallRow): MarketCall & { createdAt: string } {
return {
id: row.id,
title: row.title,
quarter: row.quarter,
date: row.date,
thesis: row.thesis,
tickers: JSON.parse(row.tickers),
snapshot: JSON.parse(row.snapshot),
createdAt: row.created_at,
} as MarketCall & { createdAt: string };
}
}
@@ -0,0 +1,74 @@
import { DatabaseConnection } from '../db/index';
import { QueryBuilder } from '../utils/QueryBuilder';
import { sanitizeTicker, sanitizeNumber } from '../utils/sanitizer';
import type { PortfolioData, PortfolioHolding, HoldingRow } from '../types';
export class PortfolioRepository {
constructor(private readonly db: DatabaseConnection) {}
/**
* Check if portfolio has any holdings.
*/
exists(): boolean {
const qb = new QueryBuilder('HOLDINGS_QUERIES.EXISTS');
const row = this.db.get<{ n: number }>(qb);
return row ? row.n > 0 : false;
}
/**
* Read all holdings.
*/
read(): PortfolioData {
const qb = new QueryBuilder('HOLDINGS_QUERIES.SELECT_ALL');
const rows = this.db.all<HoldingRow>(qb);
return { holdings: rows.map(PortfolioRepository.toHolding) };
}
/**
* Insert or update a holding (UPSERT).
*/
upsert(entry: PortfolioHolding): PortfolioHolding {
// Sanitize inputs
const ticker = sanitizeTicker(entry.ticker);
const shares = sanitizeNumber(entry.shares, 'shares', { min: 0 });
const costBasis = sanitizeNumber(entry.costBasis ?? 0, 'costBasis', { min: 0 });
const type = entry.type ?? 'stock';
const source = entry.source ?? 'Manual';
const qb = new QueryBuilder('HOLDINGS_QUERIES.UPSERT', [
ticker,
shares,
costBasis,
type,
source,
]);
this.db.run(qb);
return { ...entry, ticker };
}
/**
* Delete a holding by ticker.
*/
remove(ticker: string): boolean {
// Sanitize input
const sanitizedTicker = sanitizeTicker(ticker);
const qb = new QueryBuilder('HOLDINGS_QUERIES.DELETE_BY_TICKER', [sanitizedTicker]);
const changes = this.db.run(qb);
return changes > 0;
}
/**
* Convert database row to domain object.
*/
private static toHolding(row: HoldingRow): PortfolioHolding {
return {
ticker: row.ticker,
shares: row.shares,
costBasis: row.cost_basis,
type: row.type as PortfolioHolding['type'],
source: row.source,
};
}
}