Summary
Filter artifacts are restored from storage without any check of their shape, and FilterHandler has no way to parse one. A stored record written by an older release, a hand-edited URL parameter, or a saved filter preset from a database all reach handler.toWhere() and isActive() as whatever JSON.parse returned.
Environment
@contember/bindx@0.1.52 / @contember/bindx-dataview@0.1.52
contember/bindx@main as of 78c1712
Reproduction
// stateStorage: 'url', storageKey: 'grid'; the URL carries ?grid:filters={"title":{}}
const filtering = useFilteringState({
filters: new Map([['title', { handler: createTextFilterHandler('title') }]]),
stateStorage: 'url',
storageKey: 'grid',
})
filtering.resolvedWhere // the text handler reads `artifact.query.length` on `{}` and throws
createWebStorage / createUrlStorage (packages/bindx-dataview/src/stateStorage.ts) return JSON.parse(raw) as T, and useFilteringState (packages/bindx-dataview/src/useDataViewState.ts) uses the record as is.
An application that stores filter presets itself (restored through setAllArtifacts) has the same problem one level up: FilterArtifact is a closed union, so turning a parsed JSON object into a Record<string, FilterArtifact> needs either a cast (raw as unknown as Record<string, FilterArtifact>) or a re-implementation of every built-in artifact's shape outside bindx.
Expected behavior
- A stored artifact that the handler does not recognize is treated as absent (the filter starts at its default), not passed to the handler.
- An application can validate an artifact against the handler that will read it, without re-implementing bindx's artifact shapes.
Proposal
Add a parser to the handler contract:
interface FilterHandler<TArtifact = FilterArtifact> {
toWhere(artifact: TArtifact): Record<string, unknown> | undefined
isActive(artifact: TArtifact): boolean
defaultArtifact(): TArtifact
/** The artifact `raw` represents, or `undefined` when it is not one this handler reads. */
parseArtifact?(raw: unknown): TArtifact | undefined
}
Open design questions for the maintainers: optional (non-breaking for custom handlers, which then keep today's trust) or required; and whether setAllArtifacts should take unparsed input or keep its typed signature with a separate parseArtifacts(record) helper on the filtering state.
Summary
Filter artifacts are restored from storage without any check of their shape, and
FilterHandlerhas no way to parse one. A stored record written by an older release, a hand-edited URL parameter, or a saved filter preset from a database all reachhandler.toWhere()andisActive()as whateverJSON.parsereturned.Environment
@contember/bindx@0.1.52/@contember/bindx-dataview@0.1.52contember/bindx@mainas of78c1712Reproduction
createWebStorage/createUrlStorage(packages/bindx-dataview/src/stateStorage.ts) returnJSON.parse(raw)asT, anduseFilteringState(packages/bindx-dataview/src/useDataViewState.ts) uses the record as is.An application that stores filter presets itself (restored through
setAllArtifacts) has the same problem one level up:FilterArtifactis a closed union, so turning a parsed JSON object into aRecord<string, FilterArtifact>needs either a cast (raw as unknown as Record<string, FilterArtifact>) or a re-implementation of every built-in artifact's shape outside bindx.Expected behavior
Proposal
Add a parser to the handler contract:
create*FilterHandler).useFilteringState, run each stored entry through its handler'sparseArtifactwhen present and drop entries it rejects (they then resolve like a missing name — see Grid filter cannot start at a non-default artifact; a stored record missing a filter disagrees with the query #125).setAllArtifactscould acceptRecord<string, unknown>and parse the same way, which removes the cast on the application side entirely.Open design questions for the maintainers: optional (non-breaking for custom handlers, which then keep today's trust) or required; and whether
setAllArtifactsshould take unparsed input or keep its typed signature with a separateparseArtifacts(record)helper on the filtering state.