Files
2026-06-11 19:18:19 -04:00

386 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SQL Query Constants
*
* All SQL queries used in the application.
* Repositories reference these by name.
*
* All queries use parameterized statements (?) for security.
* User input NEVER goes into the SQL string.
*/
// ── Holdings Table Queries ───────────────────────────────────────────────────
export const HOLDINGS_QUERIES = {
// Check if any holdings exist for a user
EXISTS: 'SELECT COUNT(*) AS n FROM holdings WHERE user_id = ?',
// Get all holdings for a user, sorted by ticker
SELECT_ALL: `
SELECT ticker, shares, cost_basis, type, source
FROM holdings
WHERE user_id = ?
ORDER BY ticker ASC
`,
// Insert or update a holding scoped to a user
UPSERT: `
INSERT INTO holdings (ticker, shares, cost_basis, type, source, user_id)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(ticker, user_id) DO UPDATE SET
shares = excluded.shares,
cost_basis = excluded.cost_basis,
type = excluded.type,
source = excluded.source
`,
// Delete a holding by ticker for a specific user
DELETE_BY_TICKER: 'DELETE FROM holdings WHERE ticker = ? AND user_id = ?',
// Migrate ownerless holdings to admin user (one-time)
MIGRATE_TO_ADMIN: "UPDATE holdings SET user_id = ? WHERE user_id IS NULL OR user_id = ''",
};
// ── Market Calls Table Queries ───────────────────────────────────────────────
export const MARKET_CALLS_QUERIES = {
// Get all market calls, newest first
SELECT_ALL: `
SELECT id, title, quarter, date, thesis, tickers, snapshot, created_at
FROM market_calls
ORDER BY created_at DESC
`,
// Get a single market call by ID
SELECT_BY_ID: `
SELECT id, title, quarter, date, thesis, tickers, snapshot, created_at
FROM market_calls
WHERE id = ?
`,
// Insert a new market call
INSERT: `
INSERT INTO market_calls (id, title, quarter, date, thesis, tickers, snapshot, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
// Delete a market call by ID
DELETE_BY_ID: 'DELETE FROM market_calls WHERE id = ?',
};
// ── Migration Queries (for DatabaseInitializer) ──────────────────────────────
export const MIGRATION_QUERIES = {
// Insert holdings during migration
HOLDINGS_INSERT_OR_IGNORE: `
INSERT OR IGNORE INTO holdings (ticker, shares, cost_basis, type, source, user_id)
VALUES (?, ?, ?, ?, ?, ?)
`,
// Insert market calls during migration
MARKET_CALLS_INSERT_OR_IGNORE: `
INSERT OR IGNORE INTO market_calls (id, title, quarter, date, thesis, tickers, snapshot, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
};
// ── User Table Queries ───────────────────────────────────────────────────────
export const USER_QUERIES = {
SELECT_BY_EMAIL: `
SELECT id, email, password_hash, role, created_at, last_login
FROM users WHERE email = ?
`,
SELECT_BY_ID: `
SELECT id, email, role, created_at, last_login
FROM users WHERE id = ?
`,
INSERT: `
INSERT INTO users (id, email, password_hash, role, created_at)
VALUES (?, ?, ?, ?, ?)
`,
UPDATE_LAST_LOGIN: `
UPDATE users SET last_login = ? WHERE id = ?
`,
};
// ── Password Reset Token Queries ─────────────────────────────────────────────
export const RESET_TOKEN_QUERIES = {
INSERT: `
INSERT INTO password_reset_tokens (token, user_id, expires_at)
VALUES (?, ?, ?)
`,
FIND: `
SELECT token, user_id, expires_at, used
FROM password_reset_tokens
WHERE token = ?
`,
MARK_USED: `
UPDATE password_reset_tokens SET used = 1 WHERE token = ?
`,
// Clean up expired/used tokens older than 24h
PURGE: `
DELETE FROM password_reset_tokens
WHERE used = 1 OR expires_at < ?
`,
};
// ── Schema Definition (DDL) ──────────────────────────────────────────────────
// ── Watchlist Queries ────────────────────────────────────────────────────────
export const WATCHLIST_QUERIES = {
SELECT_ALL: `
SELECT ticker, pinned_at
FROM watchlist
WHERE user_id = ?
ORDER BY pinned_at DESC
`,
INSERT: `
INSERT OR IGNORE INTO watchlist (ticker, user_id, pinned_at)
VALUES (?, ?, ?)
`,
DELETE: `
DELETE FROM watchlist WHERE ticker = ? AND user_id = ?
`,
EXISTS: `
SELECT 1 FROM watchlist WHERE ticker = ? AND user_id = ?
`,
};
// ── Screening Universe Queries (bin/daily-screen.ts) ────────────────────────
export const UNIVERSE_QUERIES = {
// Every ticker pinned by any user
DISTINCT_WATCHLIST_TICKERS: 'SELECT DISTINCT ticker FROM watchlist ORDER BY ticker',
// Every ticker held by any user (crypto excluded — not fundamentally scored)
DISTINCT_HOLDING_TICKERS: `
SELECT DISTINCT ticker FROM holdings
WHERE type != 'crypto'
ORDER BY ticker
`,
// Every ticker screened recently (snapshot ledger) — part of the news universe
DISTINCT_SNAPSHOT_TICKERS_SINCE: `
SELECT DISTINCT ticker FROM signal_snapshots
WHERE snapshot_date >= ?
ORDER BY ticker
`,
};
// ── News Queries (FREE-DATA-STACK §25 — free-tier news pipeline) ───────────
export const NEWS_QUERIES = {
// INSERT OR IGNORE — url_hash PK is the first dedupe line (returns 0 changes on dup)
INSERT_ARTICLE: `
INSERT OR IGNORE INTO news_articles
(url_hash, title_hash, ticker_list, headline, body, source, catalyst, url, published_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
// Second dedupe line: same (normalized) title seen recently → syndicated copy
TITLE_SEEN_SINCE: `
SELECT 1 FROM news_articles
WHERE title_hash = ? AND published_at >= ?
LIMIT 1
`,
INSERT_CATALYST_LINK: `
INSERT OR IGNORE INTO ticker_catalysts (ticker, day, url_hash)
VALUES (?, ?, ?)
`,
// Per-ticker daily cap check (FREE-DATA-STACK §4.4)
COUNT_TICKER_DAY: `
SELECT COUNT(*) AS n FROM ticker_catalysts
WHERE ticker = ? AND day = ?
`,
// Stories for one ticker since a given day — what the UI reads (never Yahoo live)
SELECT_TICKER_NEWS: `
SELECT a.* FROM ticker_catalysts c
JOIN news_articles a ON a.url_hash = c.url_hash
WHERE c.ticker = ? AND c.day >= ?
ORDER BY a.published_at DESC
`,
SELECT_RECENT: `
SELECT * FROM news_articles
ORDER BY published_at DESC
LIMIT ?
`,
// Retention (FREE-DATA-STACK §5): purge bodies after 90d, drop unreferenced after 18mo
PURGE_BODIES_BEFORE: `
UPDATE news_articles SET body = NULL
WHERE body IS NOT NULL AND published_at < ?
`,
DELETE_UNREFERENCED_BEFORE: `
DELETE FROM news_articles
WHERE published_at < ?
AND url_hash NOT IN (SELECT url_hash FROM ticker_catalysts)
`,
};
// ── Signal Snapshot Queries (P0.1 — signal track record) ────────────────────
export const SIGNAL_SNAPSHOT_QUERIES = {
// One row per ticker per day — repeated screens the same day keep the latest
UPSERT: `
INSERT INTO signal_snapshots (
ticker, snapshot_date, asset_type, price, signal,
fundamental_tier, fundamental_score, fundamental_label,
inflated_tier, inflated_score, inflated_label,
coverage_active, coverage_total, risk_flags, rate_regime, created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(ticker, snapshot_date) DO UPDATE SET
asset_type = excluded.asset_type,
price = excluded.price,
signal = excluded.signal,
fundamental_tier = excluded.fundamental_tier,
fundamental_score = excluded.fundamental_score,
fundamental_label = excluded.fundamental_label,
inflated_tier = excluded.inflated_tier,
inflated_score = excluded.inflated_score,
inflated_label = excluded.inflated_label,
coverage_active = excluded.coverage_active,
coverage_total = excluded.coverage_total,
risk_flags = excluded.risk_flags,
rate_regime = excluded.rate_regime,
created_at = excluded.created_at
`,
// Full history for one ticker, oldest first (for trend/backtest views)
SELECT_BY_TICKER: `
SELECT * FROM signal_snapshots
WHERE ticker = ?
ORDER BY snapshot_date ASC
`,
// All snapshots for one day (for daily diff jobs)
SELECT_BY_DATE: `
SELECT * FROM signal_snapshots
WHERE snapshot_date = ?
ORDER BY ticker ASC
`,
// Latest snapshot per ticker on or before a given date (for change detection)
SELECT_LATEST_BEFORE: `
SELECT s.* FROM signal_snapshots s
JOIN (
SELECT ticker, MAX(snapshot_date) AS d
FROM signal_snapshots
WHERE snapshot_date < ?
GROUP BY ticker
) latest ON latest.ticker = s.ticker AND latest.d = s.snapshot_date
`,
};
export const DDL = `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'viewer' CHECK (role IN ('trader', 'viewer', 'admin')),
created_at TEXT NOT NULL,
last_login TEXT
);
CREATE TABLE IF NOT EXISTS holdings (
ticker TEXT NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id),
shares REAL NOT NULL,
cost_basis REAL NOT NULL DEFAULT 0,
type TEXT NOT NULL DEFAULT 'stock',
source TEXT NOT NULL DEFAULT 'Manual',
PRIMARY KEY (ticker, user_id)
);
CREATE TABLE IF NOT EXISTS password_reset_tokens (
token TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
expires_at TEXT NOT NULL,
used INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS market_calls (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
quarter TEXT NOT NULL,
date TEXT NOT NULL,
thesis TEXT NOT NULL,
tickers TEXT NOT NULL, -- JSON array
snapshot TEXT NOT NULL, -- JSON object
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS watchlist (
ticker TEXT NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
pinned_at TEXT NOT NULL,
PRIMARY KEY (ticker, user_id)
);
CREATE TABLE IF NOT EXISTS signal_snapshots (
ticker TEXT NOT NULL,
snapshot_date TEXT NOT NULL, -- YYYY-MM-DD
asset_type TEXT NOT NULL, -- STOCK / ETF / BOND
price REAL,
signal TEXT NOT NULL, -- ✅ Strong Buy etc.
fundamental_tier TEXT NOT NULL, -- PASS / HOLD / REJECT
fundamental_score REAL,
fundamental_label TEXT,
inflated_tier TEXT NOT NULL,
inflated_score REAL,
inflated_label TEXT,
coverage_active INTEGER,
coverage_total INTEGER,
risk_flags TEXT, -- JSON array
rate_regime TEXT,
created_at TEXT NOT NULL,
PRIMARY KEY (ticker, snapshot_date)
);
CREATE INDEX IF NOT EXISTS idx_snapshots_date ON signal_snapshots(snapshot_date);
CREATE INDEX IF NOT EXISTS idx_snapshots_signal ON signal_snapshots(signal, snapshot_date);
CREATE TABLE IF NOT EXISTS news_articles (
url_hash TEXT PRIMARY KEY, -- sha256(url)
title_hash TEXT NOT NULL, -- sha256(normalized headline) — syndication dedupe
ticker_list TEXT NOT NULL, -- JSON array of matched universe tickers
headline TEXT NOT NULL,
body TEXT, -- nullable; purged after 90 days (retention job)
source TEXT NOT NULL, -- 'edgar' | 'prwire' | 'yahoo'
catalyst TEXT, -- 'earnings'|'ma'|'guidance'|'regulatory'|'macro'|NULL
url TEXT NOT NULL,
published_at TEXT NOT NULL, -- ISO timestamp
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_news_published ON news_articles(published_at DESC);
CREATE INDEX IF NOT EXISTS idx_news_title ON news_articles(title_hash, published_at);
CREATE TABLE IF NOT EXISTS ticker_catalysts (
ticker TEXT NOT NULL,
day TEXT NOT NULL, -- YYYY-MM-DD (published date)
url_hash TEXT NOT NULL REFERENCES news_articles(url_hash),
PRIMARY KEY (ticker, day, url_hash)
);
CREATE INDEX IF NOT EXISTS idx_catalysts_ticker ON ticker_catalysts(ticker, day DESC);
`;
// ── Runtime migrations (ALTER TABLE for existing DBs) ────────────────────────
// These are safe to run repeatedly — they no-op if the column already exists.
export const RUNTIME_MIGRATIONS = [
// Add user_id to holdings if upgrading from pre-auth schema
`ALTER TABLE holdings ADD COLUMN user_id TEXT NOT NULL DEFAULT '' REFERENCES users(id)`,
];