Repository navigation
Collection filters and sorts are checked against the content type #410
Copy link
Copy link
Description
Problem
Inside a typed query, filter.content is Record<string, unknown> and sort.contentField is string. So content: { stauts: 'want' } compiles and silently returns zero results, and so does status: 'wnat'. The handle knows every field name and value type, but none of it reaches the filter.
Decision
A collection's filter.content, filter.contentPresent and sort.contentField are typed from its content type C (#409):
filter.contentkeys are the content's paths: top-level field names, plus dotted paths through declared nested objects and arrays ('address.city','emails.value'). Below anopenobject or array (Record<string, unknown>,unknown[]), any suffix is allowed. Values are the field's type (the element type for an array, since matching is element-wise), ornull.filter.contentPresenttakes the same paths.sort.contentFieldtakes only top-level scalar fields, which putsdata-model.md § Sorting by a content field's "only top-level scalars" rule into the type.- Everything is derived from the content type, not the schema. Paths, enum unions,
opennodes and top-level scalars are all visible inContentOf<S>. - The type checks names against the handle's version, but matching still runs on the stored shape. A stack mid-migration can hold
@1records whosestatusis'done', which{ status: 'finished' }doesn't match. That's existing behaviour, already stated for sorting; the spec should say it for filters too.
Prototype results
A prototype built on the public StackClient API (not committed) confirmed:
- A typo'd field, a value outside an enum, a wrong value type at a nested path, a path through a scalar, a nested sort field and a typo'd sort field all fail to compile. Paths through declared objects and arrays of objects compile, and so does any suffix below an
openobject. - Recursive path types need a depth limit. Without one,
ContentPathOffails with TS2589 ("Type instantiation is excessively deep and possibly infinite"). Typing paths to a depth of 6, and accepting any suffix below that, fixes it. Array nesting must count towards the depth too, not only objects. The spec caps paths at 32 segments, so this is a typing limit, not a query limit. - Internal aliases leak into errors. Typing
contentFieldthrough a named alias made its error print the alias and its argument instead of the field names; spelled out inline, it prints'"title" | "status" | …'. Did you mean '"title"'?. LikewiseNonNullable<C[K]>leaked into filter errors asNonNullable<"want" | …>, whereRequired<C>[K]doesn't. - Measured with
tsc --pretty false:stautsin a filter is one TS2561 line of ~460 characters;'wnat'as a status filter is one TS2322 line of ~140 characters.
Actions
- Export
ContentPathOf<C>andSortableFieldOf<C>, derived from the content type, typed to a depth of 6 with any suffix accepted below that. - Type the collection's
filter.content,filter.contentPresentandsort.contentFieldwith them. Spell out inline anything that appears in a compile error rather than naming an internal alias. - Type tests: the failing cases above are
@ts-expect-error; paths through nested objects, arrays of objects and belowopennodes compile;nullis accepted as a filter value. - Spec,
data-model.md § Type handles(or the collection section Typed access through collections, with records that don't fit reported rather than thrown #409 writes): filter keys are checked against the handle's version while matching runs on the stored shape. - Changeset:
minorfor@haverstack/core.
Order
Blocked by #409. Separate because it's pure types, with its own review concerns (recursion limits, error readability). Can run in parallel with #411.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
cuibonobo commentedon Oct 11, 2026
A note from #446 (the #409 collection) for whoever picks this up.
Content filters and sorts run on content as stored, but a collection returns records migrated to the handle's version. The adapter applies
filter.content,filter.contentPresentandsort.contentFieldbefore any record is migrated. So if a migration renamed or derived a field, filtering or sorting an@2collection by its@2name silently skips the@1records, or puts them in the wrong place.#446 documents this in
data-model.md § Type handles("Content filters and sorts apply to content as stored"). It doesn't solve it. Typing the keys againstCmakes the mismatch easier to miss, because a key the compiler accepts for@2may not exist on older records. Options worth weighing here:migrateAll()Generated by Claude Code