fix (ornith-35): watchlistRepository double-encoding bug — single JSON.stringify, 13/13 tests pass

This commit is contained in:
Investor Flow Build
2026-06-30 17:54:01 -04:00
parent 97607e0bd4
commit 1007ab4ed5
62 changed files with 11617 additions and 34 deletions
+50 -24
View File
@@ -97,30 +97,34 @@ export function addSymbol(
const s = stmts(db);
const upper = symbol.toUpperCase();
// Read existing default watchlist.
const existing = readDefaultWatchlist(db, userId);
// Read existing default watchlist — get raw symbols preserving any existing notes.
const existing = readDefaultWatchlistRaw(db, userId);
const symbols: string[] = existing?.symbols ?? [];
if (symbols.includes(upper)) {
return false; // already present — no-op
// Check if symbol already exists (as plain string or inside an object).
if (existing) {
const alreadyExists = existing.symbols.some((sym) => {
if (typeof sym === 'string') return sym === upper;
return sym.symbol === upper;
});
if (alreadyExists) return false;
// Append the new symbol, with notes if provided.
if (notes) {
existing.symbols.push({ symbol: upper, notes });
} else {
existing.symbols.push(upper);
}
const now = new Date().toISOString();
s.upsert.run(existing.id, userId, 'default', JSON.stringify(existing.symbols), now, 0);
return true;
}
symbols.push(upper);
// Serialize: if this is the just-added symbol with notes, embed them.
const serialized = symbols.map((s) => {
if (s === upper && notes) {
return JSON.stringify({ symbol: s, notes });
}
return JSON.stringify(s);
});
const id = existing?.id ?? generateId();
// No existing watchlist — create a new one.
const serialized = notes ? [{ symbol: upper, notes }] : [upper];
const id = generateId();
const now = new Date().toISOString();
// Upsert: insert-or-replace by (owner_id, name).
s.upsert.run(id, userId, 'default', JSON.stringify(serialized), now, 0);
return true;
}
@@ -133,11 +137,15 @@ export function removeSymbol(
const s = stmts(db);
const upper = symbol.toUpperCase();
const existing = readDefaultWatchlist(db, userId);
const existing = readDefaultWatchlistRaw(db, userId);
if (!existing) return false;
const before = existing.symbols.length;
const remaining = existing.symbols.filter((s) => s !== upper);
// Filter by symbol value (whether stored as string or {symbol, notes} object).
const remaining = existing.symbols.filter((sym) => {
const symStr = typeof sym === 'string' ? sym : sym.symbol;
return symStr !== upper;
});
if (remaining.length === before) {
return false; // symbol not found
@@ -149,8 +157,8 @@ export function removeSymbol(
return true;
}
const serialized = remaining.map((sym) => JSON.stringify(sym));
s.updateSymbols.run(JSON.stringify(serialized), existing.id, userId);
// Single JSON.stringify — preserves existing notes on remaining symbols.
s.updateSymbols.run(JSON.stringify(remaining), existing.id, userId);
return true;
}
@@ -210,10 +218,28 @@ function readDefaultWatchlist(db: DatabaseSync, userId: string): WatchlistRow |
const row = rows[0];
return {
...row,
symbols: safeParseSymbols(String(row.symbols)).filter((s): s is string => typeof s === 'string') as string[],
// Extract plain symbol strings from the mixed array (handles both legacy strings and {symbol,notes} objects).
symbols: safeParseSymbols(String(row.symbols)).map((s) => {
if (typeof s === 'string') return s;
if (s && typeof s === 'object' && 'symbol' in s) return (s as { symbol: string }).symbol;
return '';
}).filter((s): s is string => s.length > 0),
};
}
/** Read the default watchlist raw symbols (preserving {symbol, notes} objects). */
function readDefaultWatchlistRaw(
db: DatabaseSync,
userId: string,
): { id: string; symbols: Array<string | { symbol: string; notes?: string }> } | null {
const rows = stmts(db).selectByOwnerAndName.all(userId, 'default') as unknown as WatchlistRow[];
if (rows.length === 0) return null;
const row = rows[0];
const rawSymbols = safeParseSymbols(String(row.symbols));
return { id: row.id, symbols: rawSymbols as Array<string | { symbol: string; notes?: string }> };
}
/** Generate a simple unique id. */
function generateId(): string {
return `wl_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;