fix: stop hydrate trusting the snapshot filter and sort state came back in - #42
Merged
Merged
Conversation
`hydrate` cast the state slice straight to `FilterModel`, which is a promise
to the compiler and not a check. A condition whose operator belonged to
another kind, a `set` whose `values` was not a list, or a condition missing
the value its operator needs all reached the predicate builders as a shape
they never tested, and threw while the pipeline's `$derived` was reading
them. That read happens inside the body's `{#each grid.nodes}`, so the throw
took the render pass rather than one column. Six of the seven shapes measured
against 1.3.0 brought the grid down, and every path that reaches `hydrate` is
untrusted: share links, `localStorage`, and anything handed back to
`setState`.
`sanitizeFilterModel` now rebuilds the model from the part that can be read
and drops the rest. A column left with no readable condition stops filtering,
which shows more rows rather than none. That is the deliberate call: for a
column behind a value gate it is not strictly failing safe, and a grid that
will not render is worse.
The predicates carry a second layer for a condition arriving some other way,
`applyFilterModel` included: an unknown operator, a missing value, a `set`
whose values are not a list and a kind nothing knows now pass every row
instead of throwing.
Closes #41
`hydrate` checked `Array.isArray` and then cast, which reads as a check and is not one: a null entry in that array threw on `columnId` while the pipeline was sorting. The same untrusted path as the filter model, one layer thinner. `sanitizeSortState` keeps only the entries naming a column and a direction. An unknown direction, a missing `columnId` and a plain string entry already degraded quietly; they are now dropped rather than carried.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
filtering'shydratecast the state slice straight toFilterModel. The cast is a promise to the compiler, not a runtime check, and every path that reacheshydrateis untrusted: share links,localStorage, and anything handed back tosetState. A condition whose operator belonged to another kind, asetwhosevalueswas not a list, or a condition missing the value its operator needs all reached the predicate builders as a shape they never tested, and threw while the pipeline's$derivedwas reading them.That read happens inside the body's
{#each grid.nodes}, so the throw took the whole render pass rather than one column. Six of the seven shapes measured against 1.3.0 brought the grid down.sorting'shydratehad a thinner version of the same defect: it checkedArray.isArrayand then cast, so a null entry threw oncolumnId.Closes #41
Changes
Boundary layer.
sanitizeFilterModelrebuilds the model from the part that can be read and drops the rest: akindoutside the five, anopoutside the list for that kind, asetwhosevaluesis not an array, an operator that needs a value and has none. A group keeps its readable conditions and is dropped once it has none.sanitizeSortStatekeeps only the entries naming a column and a direction.Predicate layer. For a condition arriving some other way,
applyFilterModelincluded: the number comparator is looked up through a widened alias and checked, the text and date switches take a default branch,setPredicatechecks its array, andentryPredicatechecks its condition list. All of them pass every row instead of throwing.Either layer alone stops the crash. Both mean a future bug down a different path does not land in the pipeline either.
Behaviour changes
compileColumnFiltersalready says such a column needs its filter taken off by policy rather than by the gate. A grid that will not render is worse.{ kind: 'boolean', value: 'yes' }used to return zero rows and now returns every row, for the same reason.src/lib/features/index.ts.Known remaining edge
describeFilterwas left alone and still throws on asetwhosevaluesis not an array, returnsundefinedfor an unknown kind, and writesContains "undefined"for a missing value. The only way there now is an app callingapplyFilterModeldirectly with broken data and the grid then drawing a chip for it; thesetStatepath is sanitized before it gets that far. Out of scope for this fix, and worth its own issue.Checklist
pnpm check: 1503 files, 0 errors, 0 warningspnpm lint: cleanpnpm test: 105 files, 1323 passed, 13 skippedfilter-sanitize.test.ts(the measured table throughsetState, the same table throughapplyFilterModel, and the sanitizer's own units), 4 forsanitizeSortState, 1 grid-level sort hydrate casebudgets.test.tspasses; the added work is all at predicate build time, not in the per-row loop, so no separate bench run