179 lines
5.8 KiB
TypeScript
179 lines
5.8 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) ──────────────────────────────────────────────────
|
|
|
|
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
|
|
);
|
|
`;
|
|
|
|
// ── 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)`,
|
|
];
|