Skip to content

feat(agent-bff): expose full-text search on the list and count endpoints - #1843

Open
nbouliol wants to merge 1 commit into
mainfrom
feature/prd-961-expose-full-text-search-on-the-bff-list-and-count-endpoints
Open

feat(agent-bff): expose full-text search on the list and count endpoints#1843
nbouliol wants to merge 1 commit into
mainfrom
feature/prd-961-expose-full-text-search-on-the-bff-list-and-count-endpoints

Conversation

@nbouliol

@nbouliol nbouliol commented Aug 21, 2026

Copy link
Copy Markdown
Member

Fixes PRD-961

What

A BFF client can now run the agent's native full-text search by sending search and searchExtended on POST /agent/v1/{collection}/list and /count, instead of rebuilding a per-field Or tree of IContains/Equal that cannot cover relations or non-text columns.

  • search?: string and searchExtended?: boolean on ListRequestBody and CountRequestBody, validated by a shared assertValidSearch in both parsers.
  • Both builders emit them under the wire names the agent reads (search, searchExtended) via a shared applySearch.
  • OpenAPI: new Search and SearchExtended components on ListRequest and CountRequest.

Decisions

The ticket's open question was settled before implementing (see the Linear comment for the full reasoning):

  • Non-searchable collections: pass-through. The agent answers 400 validation_error with Collection is not searchable. No searchable flag added to /_internal/capabilities, no agent change. A fail-closed variant would read the published Forest schema, which can be stale relative to the running agent, and would reject searches the agent would accept.
  • Blank search is treated as absent. The parser validates the type; the builder decides emission via trim(). This also smooths over an agent-side inconsistency: parseSearch guards on a truthy value, so '' passes while ' ' raises on a non-searchable collection. A cleared search box must not depend on how many spaces it holds.
  • searchExtended is a strict boolean. 'true', 1, '0' are rejected with invalid_request, consistent with page.limit requiring a real integer. The agent's string coercion exists because it reads query params; the BFF is a JSON contract.
  • searchExtended alone is ignored, not rejected, and never emitted — so a search-less body produces the exact query it does today.

Relations gained search, and the ticket's non-goal is void

parseRelationListRequest delegates to parseListRequest, and the relation handlers call the same builders. Adding search to the type and the builder exposes it on /relations/{relation}/list and /count automatically — and it works agent-side, since both relation routes build their filter through ContextFilterFactory on the foreign collection. Rather than add code to forbid something the agent already supports correctly, this PR accepts it: the relation OpenAPI schemas and tests are updated alongside the top-level ones.

Note the resulting asymmetry, documented in the SearchExtended description: searchExtended reads relation fields, while naming a relation field path in filter/sort/projection is rejected with relation_field_not_supported. That guard exists for the flat response shape, not for isolation — permissions and scope stay enforced agent-side per foreign collection.

Known gap, deliberately out of scope

The agent returns meta.decorators on list — per record, which attributes matched. The BFF loses it twice: JSON:API deserialization drops the root meta, and mapListResponse only builds { data, meta: { countStatus } }. So search filters correctly but the client cannot tell why a record matched.

Carrying it through means exposing a deserializer from agent-client (jsonapi-serializer is not an agent-bff dependency), changing the AgentDataClient interface and the ListResponse contract, plus a cross-package bump. And the data is low quality: decorators are computed by naive substring match over already-serialized attributes, so a native-search datasource (the Zendesk case) or a replaceSearch yields wrong or empty decorators, and a searchExtended match in a relation yields none.

PRD-962 should confirm whether the Zendesk data tab needs highlighting for parity with the pre-BFF behaviour.

Tests

71 suites, 1137 tests green from packages/agent-bff, which is how CI runs jest.

  • Unit (agent-query.test.ts): parsers accept/reject/absent, builders' exact outgoing parameter names.
  • Route-level (data-routes-middleware.test.ts): list, count, relation list and relation count, asserting the exact query reaching the data client — including that a search-less body is byte-identical to today.
  • Integration against a real in-process agent (search-agent-integration.test.ts): AC fix(interfaces): make schema fields optional and fix typos #3 ("the count reflects the searched rows") is not provable against a stub, so this boots a real agent with createTestableAgent over an ad hoc in-memory fixture and asserts observed behaviour:
    • count without search → 3, with search: 'foundation' → 1
    • search: 'asimov' alone → 0 records; with searchExtended: true → the 2 books by that author, matched through the relation
    • search intersects the filter rather than replacing it
    • a collection with .disableSearch() is rejected 400 validation_error on both list and count, rather than listed in full

The fixture is ad hoc rather than datasource-dummy on purpose: dummy's operator set omits IContains (the operator the search decorator prefers for a String column) and it declares no relation at all, so searchExtended would have nothing to walk.

New devDependencies on agent-bff: @forestadmin/agent, @forestadmin/agent-testing.

Unrelated finding

yarn test at the repo root is broken on main, independently of this PR: packages/forest-cloud/test/commands/__mocks__/form-data.ts is a manual mock for a node module, which jest applies project-wide when run from the root. Superagent then evaluates x instanceof FormData against an ESM namespace object and 94 supertest assertions fail. CI is green because it runs jest per package. Worth a separate fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SktMf1BRjkFKR3gY25ngJt

Note

Add search and searchExtended to agent-bff list and count endpoints

  • Exposes optional search (string) and searchExtended (boolean) fields on ListRequestBody and CountRequestBody, forwarding them to the underlying agent query when the search string is non-blank after trim.
  • Adds assertValidSearch validation in parseListRequest and parseCountRequest, rejecting non-string search or non-boolean searchExtended with a 400 invalid_request.
  • Updates OpenAPI schemas (schemas.ts) to document the new fields, including blank-search handling and that searchExtended widens search to related collections.
  • Adds @forestadmin/agent and @forestadmin/agent-testing as test dependencies, plus integration tests backed by an in-memory datasource covering search, searchExtended, filter intersection, and non-searchable collection rejections.
  • Risk: blank or whitespace-only search is silently treated as absent; searchExtended is only forwarded when a non-blank search is present. Collections that disable search will cause the agent to reject the request at runtime.

Macroscope summarized efb6c2c.

A BFF client can now run the agent's native search by sending `search` and
`searchExtended` on list and count, instead of rebuilding a per-field condition
tree that cannot cover relations or non-text columns.

Both parsers validate the two fields; both builders emit them under the wire
names the agent reads. A blank search is dropped rather than forwarded, and
`searchExtended` only ships alongside a real search, so a search-less body
produces the exact query it does today.

Non-searchable collections are left to the agent: it answers 400
validation_error with "Collection is not searchable".

Relation list and count share the parsers and the builders, so they gain search
too; their OpenAPI schemas and tests are updated accordingly.

Covered by unit tests on the parsers and builders, route-level tests on all four
endpoints, and an integration test against a real in-process agent proving the
count reflects the searched rows and that a search-disabled collection is
rejected rather than listed in full.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SktMf1BRjkFKR3gY25ngJt
@linear-code

linear-code Bot commented Aug 21, 2026

Copy link
Copy Markdown

PRD-961

@qltysh

qltysh Bot commented Aug 21, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (2)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/agent-bff/src/data/agent-query.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent-bff/src/openapi/schemas.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant