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
97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
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 };
|
|
}
|
|
}
|