feat(agent-bff): expose full-text search on the list and count endpoints - #1843
Open
nbouliol wants to merge 1 commit into
Open
Conversation
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
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (2)
🛟 Help
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Fixes PRD-961
What
A BFF client can now run the agent's native full-text search by sending
searchandsearchExtendedonPOST /agent/v1/{collection}/listand/count, instead of rebuilding a per-fieldOrtree ofIContains/Equalthat cannot cover relations or non-text columns.search?: stringandsearchExtended?: booleanonListRequestBodyandCountRequestBody, validated by a sharedassertValidSearchin both parsers.search,searchExtended) via a sharedapplySearch.SearchandSearchExtendedcomponents onListRequestandCountRequest.Decisions
The ticket's open question was settled before implementing (see the Linear comment for the full reasoning):
400 validation_errorwithCollection is not searchable. Nosearchableflag 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.trim(). This also smooths over an agent-side inconsistency:parseSearchguards 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.searchExtendedis a strict boolean.'true',1,'0'are rejected withinvalid_request, consistent withpage.limitrequiring a real integer. The agent's string coercion exists because it reads query params; the BFF is a JSON contract.searchExtendedalone 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
parseRelationListRequestdelegates toparseListRequest, and the relation handlers call the same builders. Adding search to the type and the builder exposes it on/relations/{relation}/listand/countautomatically — and it works agent-side, since both relation routes build their filter throughContextFilterFactoryon 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
SearchExtendeddescription:searchExtendedreads relation fields, while naming a relation field path infilter/sort/projectionis rejected withrelation_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.decoratorson list — per record, which attributes matched. The BFF loses it twice: JSON:API deserialization drops the rootmeta, andmapListResponseonly 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-serializeris not anagent-bffdependency), changing theAgentDataClientinterface and theListResponsecontract, 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 areplaceSearchyields wrong or empty decorators, and asearchExtendedmatch 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 testsgreen frompackages/agent-bff, which is how CI runs jest.agent-query.test.ts): parsers accept/reject/absent, builders' exact outgoing parameter names.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.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 withcreateTestableAgentover an ad hoc in-memory fixture and asserts observed behaviour:search: 'foundation'→ 1search: 'asimov'alone → 0 records; withsearchExtended: true→ the 2 books by that author, matched through the relation.disableSearch()is rejected400 validation_erroron both list and count, rather than listed in fullThe fixture is ad hoc rather than
datasource-dummyon purpose: dummy's operator set omitsIContains(the operator the search decorator prefers for a String column) and it declares no relation at all, sosearchExtendedwould have nothing to walk.New devDependencies on
agent-bff:@forestadmin/agent,@forestadmin/agent-testing.Unrelated finding
yarn testat the repo root is broken onmain, independently of this PR:packages/forest-cloud/test/commands/__mocks__/form-data.tsis a manual mock for a node module, which jest applies project-wide when run from the root. Superagent then evaluatesx instanceof FormDataagainst 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
searchandsearchExtendedto agent-bff list and count endpointssearch(string) andsearchExtended(boolean) fields onListRequestBodyandCountRequestBody, forwarding them to the underlying agent query when the search string is non-blank after trim.assertValidSearchvalidation inparseListRequestandparseCountRequest, rejecting non-stringsearchor non-booleansearchExtendedwith a 400invalid_request.searchExtendedwidens search to related collections.@forestadmin/agentand@forestadmin/agent-testingas test dependencies, plus integration tests backed by an in-memory datasource covering search,searchExtended, filter intersection, and non-searchable collection rejections.searchis silently treated as absent;searchExtendedis only forwarded when a non-blanksearchis present. Collections that disable search will cause the agent to reject the request at runtime.Macroscope summarized efb6c2c.