285 lines
9.6 KiB
TypeScript
285 lines
9.6 KiB
TypeScript
/**
|
|
* 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 = ?
|
|
`,
|
|
};
|
|
|
|
// ── 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);
|
|
`;
|
|
|
|
// ── 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)`,
|
|
];
|