174 lines
5.4 KiB
TypeScript
174 lines
5.4 KiB
TypeScript
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;
|
||
|
|
}
|