Files
investor-flow/app/server/src/db/alertSubscriptionRepository.ts
T

174 lines
5.4 KiB
TypeScript
Raw Normal View History

import type { DatabaseSync } from 'node:sqlite';
import { randomUUID } from 'node:crypto';
export interface AlertSubscription {
id: string;
owner_id: string;
watchlist_id: string | null;
symbol: string | null;
alert_type: string;
enabled: boolean;
params: string;
last_fired: string | null;
created_at: string;
}
export interface CreateSubscriptionInput {
watchlistId?: string;
symbol?: string;
alertType: string;
params?: string;
}
function stmts(db: DatabaseSync) {
return {
insert: db.prepare(
`INSERT INTO alerts (id, owner_id, watchlist_id, symbol, alert_type, enabled, params, created_at)
VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
),
selectById: db.prepare(
`SELECT id, owner_id, watchlist_id, symbol, alert_type, enabled, params, last_fired, created_at
FROM alerts WHERE id = ? AND owner_id = ?`,
),
selectByOwner: db.prepare(
`SELECT id, owner_id, watchlist_id, symbol, alert_type, enabled, params, last_fired, created_at
FROM alerts WHERE owner_id = ?
ORDER BY created_at DESC`,
),
selectByOwnerAndSymbol: db.prepare(
`SELECT id, owner_id, watchlist_id, symbol, alert_type, enabled, params, last_fired, created_at
FROM alerts WHERE owner_id = ? AND symbol = ?
ORDER BY created_at DESC`,
),
selectTickerLevel: db.prepare(
`SELECT id, owner_id, watchlist_id, symbol, alert_type, enabled, params, last_fired, created_at
FROM alerts
WHERE owner_id = ? AND alert_type = ? AND symbol = ? AND enabled = 1
LIMIT 1`,
),
selectGlobalDefault: db.prepare(
`SELECT id, owner_id, watchlist_id, symbol, alert_type, enabled, params, last_fired, created_at
FROM alerts
WHERE owner_id = ? AND alert_type = ? AND enabled = 1
AND symbol IS NULL AND watchlist_id IS NULL
LIMIT 1`,
),
updateEnabled: db.prepare(
`UPDATE alerts SET enabled = ? WHERE id = ? AND owner_id = ?`,
),
updateParams: db.prepare(
`UPDATE alerts SET params = ? WHERE id = ? AND owner_id = ?`,
),
delete: db.prepare(
`DELETE FROM alerts WHERE id = ? AND owner_id = ?`,
),
};
}
function mapRow(row: Record<string, unknown>): AlertSubscription {
return {
id: row.id as string,
owner_id: row.owner_id as string,
watchlist_id: row.watchlist_id as string | null,
symbol: row.symbol as string | null,
alert_type: row.alert_type as string,
enabled: (row.enabled as number) === 1,
params: row.params as string,
last_fired: row.last_fired as string | null,
created_at: row.created_at as string,
};
}
export function createAlertSubscription(
db: DatabaseSync,
userId: string,
input: CreateSubscriptionInput,
): AlertSubscription {
const s = stmts(db);
const id = randomUUID();
const now = new Date().toISOString();
const params = input.params ?? '{}';
s.insert.run(id, userId, input.watchlistId ?? null, input.symbol ?? null, input.alertType, params, now);
return getAlertSubscription(db, userId, id)!;
}
export function getAlertSubscription(
db: DatabaseSync,
userId: string,
id: string,
): AlertSubscription | null {
const s = stmts(db);
const row = s.selectById.get(id, userId) as Record<string, unknown> | undefined;
if (!row) return null;
return mapRow(row);
}
export function listAlertSubscriptions(
db: DatabaseSync,
userId: string,
symbol?: string,
): AlertSubscription[] {
const s = stmts(db);
let rows: Record<string, unknown>[];
if (symbol) {
rows = s.selectByOwnerAndSymbol.all(userId, symbol) as Record<string, unknown>[];
} else {
rows = s.selectByOwner.all(userId) as Record<string, unknown>[];
}
return rows.map(mapRow);
}
export function updateAlertSubscription(
db: DatabaseSync,
userId: string,
id: string,
updates: { enabled?: boolean; params?: string },
): AlertSubscription | null {
const s = stmts(db);
if (updates.enabled !== undefined) {
s.updateEnabled.run(updates.enabled ? 1 : 0, id, userId);
}
if (updates.params !== undefined) {
s.updateParams.run(updates.params, id, userId);
}
return getAlertSubscription(db, userId, id);
}
export function deleteAlertSubscription(
db: DatabaseSync,
userId: string,
id: string,
): boolean {
const s = stmts(db);
const result = s.delete.run(id, userId);
return (result as { changes: number }).changes > 0;
}
export function getEffectiveAlertSubscription(
db: DatabaseSync,
userId: string,
symbol: string,
alertType: string,
watchlistIds: string[] = [],
): AlertSubscription | null {
const s = stmts(db);
// 1. Ticker-level wins.
const tickerRow = s.selectTickerLevel.get(userId, alertType, symbol) as Record<string, unknown> | undefined;
if (tickerRow) return mapRow(tickerRow);
// 2. Check list-level for each watchlist the symbol belongs to.
for (const wlId of watchlistIds) {
const s2 = db.prepare(
`SELECT id, owner_id, watchlist_id, symbol, alert_type, enabled, params, last_fired, created_at
FROM alerts
WHERE owner_id = ? AND alert_type = ? AND watchlist_id = ? AND symbol IS NULL AND enabled = 1
LIMIT 1`,
);
const row = s2.get(userId, alertType, wlId) as Record<string, unknown> | undefined;
if (row) return mapRow(row);
}
// 3. Global default.
const globalRow = s.selectGlobalDefault.get(userId, alertType) as Record<string, unknown> | undefined;
if (globalRow) return mapRow(globalRow);
return null;
}