Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
## 2026-07-20 - [Prisma Distinct requires matching OrderBy on PostgreSQL]
**Learning:** When using `distinct` combined with `orderBy` in Prisma, PostgreSQL translates this into a `DISTINCT ON` SQL clause. This strictly requires that the distinct field(s) be the first argument(s) in the `orderBy` array.
**Action:** Always prepend the `distinct` column(s) to the `orderBy` array when using Prisma deduplication to avoid fatal runtime errors in PostgreSQL environments.
## 2026-07-26 - [N+1 query problem inside Promise.allSettled loop]
**Learning:** Performing database queries inside parallel loops like `Promise.allSettled` over multiple entities (e.g. accounts) can cause N+1 query problems and significantly impact backend performance, especially when there are many entities.
**Action:** Extract database lookups inside parallel promise loops into a single batch query executed before the loop. Index the batch results into a `Map` by entity ID for O(1) lookups inside the loop.
5 changes: 4 additions & 1 deletion server/controllers/PoolController.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class PoolController extends BaseController {
try {
const allAccounts = await store.getAllAccountsWithKeys(req.user.id);

// ⚑ Bolt: Batch fetch local keys for all accounts to avoid N+1 query problem inside Promise.allSettled loop
const batchedLocalKeysMap = await store.getLocalKeysForAccounts(req.user.id, allAccounts.map(a => a.id));

const accountResults = await Promise.allSettled(
allAccounts.map(async (account) => {
throwIfAborted(requestAbort.signal);
Expand Down Expand Up @@ -87,7 +90,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 = batchedLocalKeysMap.get(account.id) || [];
const localMap = new Map(localKeys.map((k) => [k.hash, k]));

const enrichedKeys = liveKeys.map((k) => {
Expand Down
28 changes: 28 additions & 0 deletions server/services/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,34 @@ 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 keysByAccount = new Map();
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 enrichedKey = { ...keyRecord, key };
if (!keysByAccount.has(keyRecord.accountId)) {
keysByAccount.set(keyRecord.accountId, []);
}
keysByAccount.get(keyRecord.accountId).push(enrichedKey);
}

return keysByAccount;
}

export async function registerKeyString(userId, hash, rawKeyString) {
const normalizedKey = rawKeyString?.trim();
if (!normalizedKey) {
Expand Down