phase-10.5: market screener ui enhancements

This commit is contained in:
saikiranvella
2026-06-09 01:21:02 -04:00
parent 3c321a4a79
commit 5c8cd8935a
45 changed files with 3054 additions and 539 deletions
@@ -1,34 +1,33 @@
import { DatabaseConnection } from '../db/index';
import { QueryBuilder } from '../utils/QueryBuilder';
import { sanitizeTicker, sanitizeNumber } from '../utils/sanitizer';
import type { PortfolioData, PortfolioHolding, HoldingRow } from '../types';
import { DatabaseConnection } from '../db/index.js';
import { QueryBuilder } from '../utils/QueryBuilder.js';
import { sanitizeTicker, sanitizeNumber } from '../utils/sanitizer.js';
import type { PortfolioData, PortfolioHolding, HoldingRow } from '../types/index.js';
export class PortfolioRepository {
constructor(private readonly db: DatabaseConnection) {}
/**
* Check if portfolio has any holdings.
* Check if a user has any holdings.
*/
exists(): boolean {
const qb = new QueryBuilder('HOLDINGS_QUERIES.EXISTS');
exists(userId: string): boolean {
const qb = new QueryBuilder('HOLDINGS_QUERIES.EXISTS', [userId]);
const row = this.db.get<{ n: number }>(qb);
return row ? row.n > 0 : false;
}
/**
* Read all holdings.
* Read all holdings for a user.
*/
read(): PortfolioData {
const qb = new QueryBuilder('HOLDINGS_QUERIES.SELECT_ALL');
read(userId: string): PortfolioData {
const qb = new QueryBuilder('HOLDINGS_QUERIES.SELECT_ALL', [userId]);
const rows = this.db.all<HoldingRow>(qb);
return { holdings: rows.map(PortfolioRepository.toHolding) };
}
/**
* Insert or update a holding (UPSERT).
* Insert or update a holding scoped to a user (UPSERT).
*/
upsert(entry: PortfolioHolding): PortfolioHolding {
// Sanitize inputs
upsert(entry: PortfolioHolding, userId: string): PortfolioHolding {
const ticker = sanitizeTicker(entry.ticker);
const shares = sanitizeNumber(entry.shares, 'shares', { min: 0 });
const costBasis = sanitizeNumber(entry.costBasis ?? 0, 'costBasis', { min: 0 });
@@ -41,6 +40,7 @@ export class PortfolioRepository {
costBasis,
type,
source,
userId,
]);
this.db.run(qb);
@@ -48,20 +48,15 @@ export class PortfolioRepository {
}
/**
* Delete a holding by ticker.
* Delete a holding by ticker for a specific user.
*/
remove(ticker: string): boolean {
// Sanitize input
remove(ticker: string, userId: string): boolean {
const sanitizedTicker = sanitizeTicker(ticker);
const qb = new QueryBuilder('HOLDINGS_QUERIES.DELETE_BY_TICKER', [sanitizedTicker]);
const qb = new QueryBuilder('HOLDINGS_QUERIES.DELETE_BY_TICKER', [sanitizedTicker, userId]);
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,