diff --git a/.jules/bolt.md b/.jules/bolt.md index a94bc51..df68c26 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,7 @@ ## 2026-06-08 - [Avoid Re-Querying Static DB Data if Cache Exists] **Learning:** In applications using frequent polling (like a live traffic dashboard), direct database queries for static or slow-changing data (like model catalogs/prices) can cause unnecessary DB load. The application already maintained an in-memory cache for models `getCachedPoolModels()`, but `getTraffic` was directly querying the DB using `prisma.cachedModel.findMany` instead of utilizing the cache. **Action:** Always check if a caching layer or service already exists for lookup/reference data before writing direct database queries, especially on high-frequency endpoints. + +## 2026-06-10 - [Avoid N+1 queries in mapping collections] +**Learning:** In applications mapping over records (`allAccounts.map`) and awaiting queries inside (`await store.getLocalKeys(req.user.id, account.id)`), an N+1 query vulnerability exists which leads to excessive database load when the collection size is large. +**Action:** Always batch related data queries prior to mapping over collections by retrieving data with an `IN` clause and aggregating them into a Map object that can be queried continuously in memory. diff --git a/server/controllers/PoolController.js b/server/controllers/PoolController.js index dd0f71b..9a46840 100644 --- a/server/controllers/PoolController.js +++ b/server/controllers/PoolController.js @@ -51,6 +51,12 @@ class PoolController extends BaseController { try { const allAccounts = await store.getAllAccountsWithKeys(req.user.id); + // ⚡ Bolt: Fetch all local keys for all accounts in one query instead of N queries + const localKeysMap = await store.getLocalKeysForAccounts( + req.user.id, + allAccounts.map((a) => a.id) + ); + const accountResults = await Promise.allSettled( allAccounts.map(async (account) => { throwIfAborted(requestAbort.signal); @@ -87,7 +93,7 @@ class PoolController extends BaseController { } // Get local DB records to merge isPooled + hasKeyString - const localKeys = await store.getLocalKeys(req.user.id, account.id); + const localKeys = localKeysMap.get(account.id) || []; const localMap = new Map(localKeys.map((k) => [k.hash, k])); const enrichedKeys = liveKeys.map((k) => { diff --git a/server/services/store.js b/server/services/store.js index f10868c..9ee3281 100644 --- a/server/services/store.js +++ b/server/services/store.js @@ -1129,6 +1129,35 @@ export async function getLocalKeys(userId, accountId) { }); } +export async function getLocalKeysForAccounts(userId, accountIds) { + if (!accountIds || accountIds.length === 0) return new Map(); + + const keys = await prisma.key.findMany({ + where: { accountId: { in: accountIds }, account: { userId } }, + }); + + const map = new Map(); + for (const accountId of accountIds) { + map.set(accountId, []); + } + + for (const keyRecord of keys) { + let key = null; + if (keyRecord.key) { + try { + key = decrypt(keyRecord.key) || null; + } catch (err) { + logger.warn(`[STORE] Failed to decrypt local key hash=${keyRecord.hash}: ${err.message}`); + key = null; + } + } + const enriched = { ...keyRecord, key }; + map.get(keyRecord.accountId).push(enriched); + } + + return map; +} + export async function registerKeyString(userId, hash, rawKeyString) { const normalizedKey = rawKeyString?.trim(); if (!normalizedKey) {