Skip to content

Filter search by field values and references, with text optional for one type - #2606

Merged
pournasserian merged 5 commits into
v2from
2599-search-filters
Oct 19, 2026
Merged

pournasserian merged 5 commits into
v2from
2599-search-filters

Conversation

@pournasserian

@pournasserian pournasserian commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Closes #2599

What

The second slice of F-SRC (#2430): search filters on field values and references, combined with text, or without text for one type (SRC-01).

  • Core (FluentCMS.Core.Search):
    • SearchQuery.Fields: filters on the latest version's field values, all of which must hold. Each is a FieldFilter(field, operator, value), and they need exactly one type in ContentTypes.
    • SearchQuery.References: an entry or entity id. Only entries whose latest version references it are kept.
    • FieldFilters checks the filters against the type's definition before anything is read, and applies them to an entry's fields:
      • Text, rich text and enum fields take equals and contains, which ignores case.
      • Number and date-time fields take equals, less_than, at_most, greater_than and at_least. Numbers are compared as decimals, as the number field type stores them. A date-time value is a point in time in ISO 8601 with an offset, or a YYYY-MM-DD date for a dateOnly field, checked with the same rules the field type uses.
      • Boolean and reference fields take equals.
      • On an entity, the fact's value is compared.
      • A missing or null field holds no filter.
      • Lists, JSON and media are refused as not filterable.
    • New codes: search_filter_needs_one_type, search_field_not_found, search_field_not_filterable, search_operator_not_supported and search_filter_value_invalid. An empty Fields list is search_filter_empty.
  • Text is optional when the query names one type and filters it by fields or a reference. The candidates are that type's entries in id order, read a page at a time like text matches, with a rank of 0 and no via hits.
  • Infrastructure: EntrySearch applies the field and reference filters to each page's candidates that pass the scope, deletions and state filters. It reads their latest versions in one query per page, and the same rules apply to entries found through a reference.
  • Tool: search_entries takes fields, an array of {field, operator, value}, and references, an id. query is optional with one type and either filter. A malformed item or an unknown operator is an argument error naming its position.
  • ToolArgumentsReader.OptionalArray.

Why

SRC-01 asks for full-text and structured search. The first slice filtered by type, state and published version. Agents next ask for things like "products under 20" or "the pages that reference this product".

Choices worth a look:

  • Field values are filtered in C# on the candidates, not in SQL. ADR-0010 says structured filters are ordinary EF queries, and the reference filter is one. The fields, though, are JSON whose shape depends on the type, and an entity's value sits inside its fact. One evaluator gives the same answer on both providers, and it runs only on candidates that already passed everything else, a page at a time. A search without words reads at most Search__MaxMatches of the type's entries and says when it stopped early. If the maintainer prefers SQL here, it can move behind the same FieldFilters rules.
  • Field filters need one type, so each field and its type are known, and a wrong operator or value is refused before anything is read instead of matching nothing.
  • Without words there is no rank and no via: every candidate is a match, so a reference has nothing to explain.
  • Not in this PR: filters on list items, JSON paths and media metadata, ordering by a field, and the desk search box (desk lane).

Tests

Unit: each field type's operators and value kinds, with their refusals, including dates without an offset or in another form. Each operator on text, decimals beyond a double's precision, date-times with offsets, calendar dates, booleans and references. A missing or null field, all filters having to hold, and an entity's fact value.

Integration, each on SQLite and PostgreSQL:

  • "widget" with price less_than 20 on an entity type finds the product at 19.99, and not those at 25 or without the word. With no words, name contains "wid" finds both widgets, with rank 0.
  • references finds the page whose latest version references the product, and not one whose earlier version did. Combined with words, it keeps only matches that also reference it.
  • Field filters with no type or two types, a reference filter with no words and no type, an empty fields list, an unknown field, contains on a number and values of the wrong kind are refused with their codes.
  • Over HTTP, on an entity type, an agent filters by the price fact, finds both products with contains and no query, and finds by reference the page whose latest version references the product, not the one whose earlier version did. An unknown operator and an incomplete item are argument errors.
  • Over HTTP, no type or two types, an unknown field, contains on a number and a value of the wrong kind each return their code.

The full suite ran locally against a postgres:17 container as well as SQLite, and markdownlint passes. No new package and no new project reference.

Checklist

  • Linked to an issue a maintainer has confirmed
  • Targets v2
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

@pournasserian pournasserian added this to the M1 Governance core milestone Oct 19, 2026
@pournasserian pournasserian added type:feature New capability v2 FluentCMS v2 (v2 branch) area:infra Persistence, host, CI and build labels Oct 19, 2026
@pournasserian
pournasserian merged commit 62f6e57 into v2 Oct 19, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2599-search-filters branch October 19, 2026 09:03
pournasserian added a commit that referenced this pull request Oct 19, 2026
Closes #2621

## What

The third slice of F-FRM (#2426): pickers for reference and media fields
(FRM-01), backed by search (`ISearch`, #2566, #2606) and the media
library (#2543).

- **`PickerField`** (`Components/Forms`) is an interactive island inside
the static generated form. `FieldWidget` renders it for reference and
media fields.
- Over the circuit it shows the current choice, a Clear button and a
search box. The choice is a hidden input named `Form.Values[field]`, so
the form posts and saves exactly as before.
- **Entries**: as the person types, it searches the reference's target
type (at most 10 direct hits). Each hit shows its first text value, or
an entity's first text fact, with its state and a short id.
- **Media**: it filters the library by file name or folder, newest
first, with a preview for images.
- The matches are a `listbox` of buttons that rove with the module from
#2585. ArrowDown moves from the search box into the list, the arrows
move through it, Enter chooses and Escape clears the matches.
- Before the circuit attaches, or when it is lost, the prerendered
island is the plain select from #2613, so a form posted then still
saves.
- **`admin.js`**: a `data-ui="rove-input"` box moves focus to the first
item of the list its `aria-controls` names on ArrowDown.

## Why

- **An island, not a page in interactive mode.** Only the search needs
the circuit. The form, its other fields and the save stay static, so the
save path from #2613 does not change, and a lost circuit costs only the
search.
- **A hidden input in the island.** The static form collects it with the
other fields, and no server state has to survive between the choice and
the save.
- **`FieldOption[]` rather than an interface.** An island's parameters
cross into the circuit as JSON. With `IReadOnlyList<FieldOption>`, the
browser refused the root components with "The list of component
operations is not valid". The parameter says why in a comment.
- **Not in this PR**: field filters in the picker (`SearchQuery.Fields`
exists for it once a field's settings name a filter), list-of-reference
widgets, and rule hints (FRM-03, needs F-RUL #2419).

## Tests

- bUnit (`PickerFieldTests`):
  - Before the circuit, a picker is the select with the stored choice.
- An entry picker searches its target type and names hits by their first
text fact. It wires the listbox to the search box and the rove module,
and choosing posts the id and shows the name. Clear empties the choice.
  - A search with no match says so, and Escape clears the matches.
- A media picker finds an asset by file name, case-insensitively, with
its preview, and posts its id.
- `GeneratedFormTests` renders the reference and media widgets before
the circuit, as selects.
- Browser (`FormPickerTests`), keyboard alone: type the title, Tab to
the picker, type part of a product's name, press ArrowDown and Enter,
then write the rationale and save. The stored entry references the
product.

`dotnet build -warnaserror`, the unit and bUnit suite, the browser tests
and the SQLite integration suite pass locally. The local PostgreSQL run
on a loaded machine had nine failures in untouched classes, the same
pattern as the last three PRs. CI runs the full PostgreSQL suite. No new
package and no new project reference.

## Checklist

- [x] Linked to an issue a maintainer has confirmed
- [x] Targets `v2`
- [x] Tests or docs updated where behaviour changed
- [x] No tool-generated co-author trailers or footers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:infra Persistence, host, CI and build type:feature New capability v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Search filters on field values and references, combined with text

1 participant