Skip to content

fix(store): guard findIndex(-1) before indexing in store actions - #3219

Open
AndyScherzinger wants to merge 2 commits into
mainfrom
fix/noid/store-findindex-guards
Open

AndyScherzinger wants to merge 2 commits into
mainfrom
fix/noid/store-findindex-guards

Conversation

@AndyScherzinger

@AndyScherzinger AndyScherzinger commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Guards every findIndex() result in src/store/store.js against the -1 (not-found) case before indexing or splicing. When the affected node was not present in the local store, a successful server call could still corrupt local state: this.X[-1].favorite = … threw a TypeError (favorite toggles), splice(index, 1) with index === -1 removed the last element (remove/transfer view, table, context), and arr[index] = view created a bogus "-1" property (updateView/reloadViewsOfTable). Each site now returns early, skips the splice, or pushes the missing item instead.

Pre-existing on main; split out of #2476 (personal archiving) per the focused-PR guideline so that PR stays scoped to archiving. src/store/data.js has the same bug in updateColumn and updatePublicRow and is fixed in #3220.

🖼️ Screenshots

n/a — no visual change (internal store guards only).

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔙 Backport requests are created or not needed: /backport to stableX.X
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@blizzz

blizzz commented Nov 4, 2026

Copy link
Copy Markdown
Member

Claude review, reduced to relevant findings

Review: #3219: guard findIndex(-1) in store actions

Scope: src/store/store.js only. The PR adds guards for the -1 (not found) case in 10 store actions. None of the untrusted files contain text that tries to give me instructions.

Discussion summary

  • Full stack dev: The new guards are correct. They match the pattern setContext already uses (store.js:128). But the PR says it guards every findIndex(), and that isn't true: several unguarded sites remain. One of them sits in a helper that the newly guarded code calls.
  • System architect: The same "find, then replace or append" logic is now written out in about 8 places. A shared helper would remove both the duplication and the gaps.

Findings (by severity)

  1. Medium: setTable still splices at -1, src/store/store.js:119-120
    this.tables.splice(index, 1, table) with index === -1 replaces the last table. This is the exact bug the PR fixes elsewhere. The favorite actions don't reach it now because of the new early return, but every other caller of setTable still can.
    Fix: if (index === -1) { this.tables.push(table); return }, the same as setContext.

  2. Medium: setView still writes views[-1], store.js:122-124
    With index === -1 this creates a stray "-1" property that the getters never see.
    Fix: the same push-or-replace guard.

  3. Medium: updateTable and updateContext are unguarded, store.js:324-325 and store.js:491-492
    this.tables[index] = table and this.contexts[index] = context both create the same bogus "-1" property the PR fixes in updateView.
    Fix: the same guard, or call the fixed setTable / setContext.

  4. Low: data.js has the same bug, out of scope but worth a follow-up

    • src/store/data.js:219-220 (updateColumn)
    • data.js:384-385 (updatePublicRow)

    Both write to [-1]. Fix: the same guard, or .map() like updateRow does at data.js:362.

  5. Low: duplicated logic
    Fix: add a small helper, e.g. upsertById(arr, item) and removeById(arr, id), and use it in all of these places so a missed spot can't happen again.

Verdict

Approve with changes requested. What the PR changes is correct and safe, but it leaves the same bug in setTable, setView, updateTable and updateContext. Those four sit right next to the fixed code, so they should be fixed in this PR to make its title true. The data.js fixes and the refactor into a helper can go in a follow-up PR.

@AndyScherzinger
AndyScherzinger force-pushed the fix/noid/store-findindex-guards branch from 0610b5a to 31aa558 Compare November 4, 2026 20:15
@AndyScherzinger

Copy link
Copy Markdown
Member Author

Findings 1–3 are fixed in 31aa558 (amended into the original commit, force-pushed):

  • setTable and setView now push-or-replace on index === -1, the same guard setContext already had.
  • updateTable and updateContext no longer assign at the index themselves; they go through setTable/setContext, so the guard lives in one place instead of being repeated at the call site. That also removes two of the duplicated find-then-replace blocks finding 5 is about, without adding a helper.

Finding 4 (data.js updateColumn/updatePublicRow) and the rest of finding 5 stay out of this PR as suggested — data.js gets its own PR.

Several store actions dereferenced or spliced a findIndex() result
without guarding the -1 case:

- favoriteView/removeFavoriteView/favoriteTable/removeFavoriteTable threw
  a TypeError at this.X[-1].favorite when toggling a favorite on a node
  absent from the local store.
- removeView/removeTable/removeContext and the transfer actions called
  splice(index, 1) with index -1, which removes the LAST element instead
  of the intended (absent) one.
- updateView/reloadViewsOfTable assigned to arr[-1], silently creating a
  bogus "-1" property instead of updating the view.
- setTable spliced at -1, replacing the LAST table with the one it was
  asked to set; setView assigned to views[-1]. Both are reached by every
  caller, not only the favorite actions guarded above.
- updateTable and updateContext assigned to tables[-1] and contexts[-1].

Return early (favorites) or skip the write, so a server success never
corrupts the local store.

An absent id means "nothing to update" rather than "insert": on a
mutation echo it is missing because the node was removed while the
request was in flight, and adding it back would resurrect it as a
navigation entry that 404s on click. setContext is the exception and
keeps its insert, which exists so that loading one application by its
URL before the list is there stays visible, so updateContext guards at
the call site instead of routing through it.

updateView and reloadViewsOfTable now delegate to setView rather than
repeating its body, and setTable assigns by index instead of splicing,
as AGENTS.md requires for Vue 3.

Assisted-by: ClaudeCode:claude-opus-4-8
Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
insertNewView looks the parent table up in this.tables and pushes the
new view into it. The lookup is outside the try block, so when the table
is not in the local store the push throws a TypeError and the action
never returns the new view id.

The table is absent whenever the view was created on a table the user
only reaches through a view share: findAll returns the tables the user
owns or has a table share on, so a directly shared view has no table
entry. Cloning such a view then reports "Could not create new view" even
though the server created it, leaving an orphan behind.

Skip the push when the table is unknown; the view is already in
this.views, which is what the navigation renders for shared views.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants