Skip to content

finding(core): convertFiltersToAST REFUSES $icontains, a canonical FILTER_OPERATORS member that ValueDataSource executes and this repo's own filter builder emits #8976

Description

@os-steve

Found by the domain:ui os-dev seat while running objectui#8568's prerequisite census (PR for that card is separate; this is out of its scope, which is measurement only and zero behaviour change). Grading and domain:* are the triage seat's. ⛔ Not claimed.

The class

objectui#8568 records that convertFiltersToAST and ValueDataSource answer one $-dialect with two acceptance sets, and names the direction where the converter is MORE tolerant (four lowercase aliases the matcher refuses).

There is a third leg, in the opposite direction, and it is not on that card: $icontains is a canonical member of the spec's FILTER_OPERATORS, ValueDataSource executes it, and convertFiltersToAST refuses it with a 400 envelope.

Measured

origin/main 3ff3fc63f, spec 17.4.0, both legs in one process, with a control operator both sides agree on so the difference is about $icontains rather than about the harness:

spec FILTER_OPERATORS includes $icontains: true
LOWERING $icontains          => THREW FilterOperatorError / INVALID_FILTER / 400
LOWERING CONTROL $contains   => ["name","contains","AC"]
MATCHER  $icontains          => rows ["ACME"]
MATCHER  CONTROL $contains   => rows ["ACME"]

The refusal message enumerates its own supported set, and $icontains is absent from it:

[ObjectUI] Unknown filter operator '$icontains' for field 'name'. Supported operators:
$eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $between, $contains, $notContains,
$startsWith, $endsWith, $null, $exists.

Why this is a contract violation and not a preference

@objectstack/spec data/filter.zod.ts declares:

export const FILTER_OPERATORS = [
  '$eq', '$ne',
  '$gt', '$gte', '$lt', '$lte',
  '$in', '$nin', '$between',
  '$contains', '$notContains', '$startsWith', '$endsWith', '$icontains',
  '$null', '$exists',
] as const;

$icontains is in it. packages/core/src/adapters/README.md goes further and names $icontains as the prescription printed when $like / $ilike / $regex are refused — so the repo tells an author to write the one spelling one of its two lowering paths rejects.

This repo is itself a producer of it

packages/fields/src/widgets/FilterConditionField.tsx emits it for the containsCaseInsensitive builder operator:

case 'containsCaseInsensitive': return { [field]: { $icontains: value } };

That arm exists because objectui#4023 / objectui#3567 (both CLOSED) fixed the case-insensitive contains being unreachable from the filter UI. kvToCondition reads it back. So an admin who picks "contains (ignore case)" writes criteria that the in-memory matcher honours and the ObjectStack lowering path refuses.

⚠️ The end-to-end reach from criteria_json into convertFiltersToAST is not provable from this repo — the criteria store is not in this tree, and this is exactly the consumer-local caveat objectui#6839 established. What IS proven here is the acceptance-set disagreement and the producer arm.

Why no existing pin catches it

PR #8565's reconciliation pin (packages/data-objectstack/src/readme-filter-operator-table.test.ts) holds the README's operator tables complete against two populations: the keys of operatorMap, and the operators the unknown-operator error calls supported. $icontains is in neither, so the pin is structurally blind to it — and packages/data-objectstack/README.md carries no row for $icontains in its supported table or its refused table. Measured: git grep -n icontains -- packages/data-objectstack/ returns nothing.

Directions (not ruled here)

  1. Add $icontains to operatorMap so the lowering matches the spec and the matcher. icontains is a member of the spec's VALID_AST_OPERATORS (it is already an identity row in FILTER_OPERATOR_ALIASES for the object-form dialect, with a comment recording that check), so nothing downstream needs inventing.
  2. Refuse it deliberately and say so — a refused-table row plus a prescription, the way $like / $regex are handled. That contradicts the adapters README, which currently prescribes $icontains.
  3. Whatever is chosen, the pin's blind spot is worth closing: it should hold the tables against the spec's FILTER_OPERATORS too, not only against the code's own two lists — the omission is invisible to it by construction.

Dedup

Two channels, each with controls.

  • REST issue listing, complete and reconciled: all 459 open items enumerated across 5 pages, open_issues_count 459 = 459 listed. Local grep of title+body: icontains hits objectui#7750 and objectui#7383, containsCaseInsensitive hits objectui#7561. Positive control ValueDataSource fires with 12 hits; negative control ZZQQNOTAWORDZZ reads 0.
  • One targeted semantic search including closed: returns objectui#4023 and objectui#3567 (both the PRODUCER side, both closed and fixed), plus objectui#8568 itself as a lit control.

None of them names the lowering path refusing $icontains: objectui#7383 is like / ilike in the matcher's AST dialect; objectui#7561 is filter-builder's dropdown-vs-mirror vocabularies; objectui#7750 is DataScopeManager. No card names this.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions