Files
wekan/models/lib/boardCardScope.js
T
Lauri Ojansivu a51afa5a1b Fix boards loading lists but never cards on FerretDB (10.22): drop the null from the card-scope $in so it stays index-backed.
Every board card query used `{ boardId: { $in: [board._id, board.subtasksDefaultBoardId] } }`,
and subtasksDefaultBoardId defaults to null. On FerretDB v1 (SQLite) a $in that
contains a non-string element (the null) does not push down at all, so the WHERE was
dropped and the whole cards collection was full-scanned + sjson-decoded on every
poll-and-diff cycle, across ~7 card cursors per board open. Lists and swimlanes use a
plain boardId equality (index-backed) and stayed fast — so on a big board the columns
appeared but the cards never did.

New helper models/lib/boardCardScope.js builds the scope WITHOUT null and prefers a
plain equality (index seek) when only the board itself is in scope, else an
all-string $in (which pushes down). Applied to every card-scope site in
server/publications/boards.js (8x), cardsWindow.js and cards.js.

Also flatten the lazy card-window selector (cardsWindow.js): the window was
`{ $and: [clientSelector, { boardId, archived }] }`, and FerretDB does not push down
a top-level $and, so boardId/archived were full-scanned too. Merge the board scope at
the top level (exactly equivalent when the client selector has no boardId/archived
key, which it does not — it is a per-list listId + swimlane selector; a collision
guard keeps $and otherwise), so boardId/archived push down.

The FerretDB fork side is hardened separately so a mixed $in (incl. null) also uses
the index. Test: tests/boardCardScope.test.cjs.

Thanks to xet7 !
2026-07-22 20:05:32 +03:00

39 lines
1.7 KiB
JavaScript

'use strict';
// The selector matching cards that belong to a board: the board itself, plus its
// subtasks-default board when one is configured.
//
// Why this helper exists (10.22 "boards load lists/columns but never cards"):
// the card queries used `{ boardId: { $in: [board._id, board.subtasksDefaultBoardId] } }`,
// and `subtasksDefaultBoardId` defaults to `null`. On FerretDB v1 (SQLite) a `$in`
// list that contains a non-string element (the `null`) does NOT push down at all,
// so the WHERE clause is dropped and the whole `cards` collection is full-scanned
// and sjson-decoded in Go on EVERY poll-and-diff cycle. Lists/swimlanes use a plain
// `boardId` equality, which is index-backed and fast — hence columns appear but
// cards never do. Build the id list WITHOUT null, and use a plain equality when
// only the board itself is in scope, so the card query stays index-backed like the
// lists selector.
function boardScopeIds(board) {
const ids = [];
if (board && typeof board._id === 'string' && board._id.length > 0) {
ids.push(board._id);
}
if (board && typeof board.subtasksDefaultBoardId === 'string' && board.subtasksDefaultBoardId.length > 0) {
ids.push(board.subtasksDefaultBoardId);
}
return ids;
}
// Returns a Mongo selector fragment `{ boardId: <id> }` (equality, index-backed)
// or `{ boardId: { $in: [id1, id2] } }` — never an $in that contains null. Spread
// it into a card selector: `{ ...boardCardScope(board), archived: false }`.
function boardCardScope(board) {
const ids = boardScopeIds(board);
if (ids.length <= 1) {
return { boardId: ids[0] };
}
return { boardId: { $in: ids } };
}
module.exports = { boardScopeIds, boardCardScope };