Give every report service an optional accountIds?: number[] filter that narrows results to a subset of import sources (transactions.source_id), matched via a parameterized IN (...) clause — one bound placeholder per id, never a joined string (CWE-89). New shared inPlaceholders() helper (src/utils/sqlFilters.ts) generates the placeholder list so the pattern lives in exactly one place. New accountIds param: getCompareMonthOverMonth, getCompareYearOverYear, getBudgetVsActualData, getCartesSnapshot (forwarded to its getCompareMonthOverMonth + getBudgetVsActualData sub-calls, so the Cartes dashboard's top-movers and budget-adherence cards respect an active filter instead of silently ignoring it). Signature change scalar -> plural: getMonthlyTrends, getCategoryOverTime, getExpensesByCategory (sourceId?: number -> accountIds?: number[]). No scalar production caller of these three passed sourceId today, so only the test call-sites needed updating to the array shape. Omitted/empty accountIds adds no clause at all, so every service stays byte-identical to its pre-#273 SQL/results — pinned by a regression test per service (new budgetService.test.ts / dashboardService.test.ts files, extended reportService.test.ts / reportService.cartes.test.ts). This is backend plumbing only: no report page exposes an account filter control yet (follow-up issues #274-#276 add the shared <FilterPanel> and wire it into Trends/Compare/Budget). Resolves #273 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
16 lines
864 B
TypeScript
16 lines
864 B
TypeScript
/**
|
|
* Shared helper for building a parameterized SQL `IN (...)` clause — one
|
|
* bound placeholder per id, never a joined/interpolated value list (CWE-89).
|
|
* First introduced for the report services' optional account
|
|
* (`transactions.source_id`) filter (Issue #273); reused by every service
|
|
* that accepts an `accountIds?: number[]` filter so the placeholder
|
|
* bookkeeping (start index, one `$N` per id) lives in exactly one place.
|
|
*
|
|
* Returns `null` when `ids` is empty/undefined so callers can skip adding the
|
|
* clause entirely — that is the "no filter" case, and the query must stay
|
|
* byte-identical to how it read before the filter existed.
|
|
*/
|
|
export function inPlaceholders(ids: number[] | undefined, startIndex: number): string | null {
|
|
if (!ids || ids.length === 0) return null;
|
|
return ids.map((_, i) => `$${startIndex + i}`).join(", ");
|
|
}
|