diff --git a/product/build/search-configuration.mdx b/product/build/search-configuration.mdx
index 0a9c82a..cd5af00 100644
--- a/product/build/search-configuration.mdx
+++ b/product/build/search-configuration.mdx
@@ -14,6 +14,8 @@ Two search modes exist:
- **Normal search**, searches fields in the current collection
- **Extended search**, also searches fields in directly related collections. Operators can trigger extended search from the footer when normal results are empty.
+Extended search reaches columns outside the current collection, so the Node.js agent checks each related collection it reads against the operator's `read` permission. From version 1.97.3 on, the agent refuses an extended search whose fields it cannot enumerate ahead of the query. This governs how you replace the search handler — see [Replacing the Search Handler](#replacing-the-search-handler).
+
## Default Search Behavior
By default, Forest searches only specific field types:
@@ -30,12 +32,32 @@ By default, Forest searches only specific field types:
Use `replaceSearch` in your back-end configuration to define exactly how search strings are translated into filters.
+The Node.js agent accepts two forms, and they differ in what extended search does:
+
+| Form | Extended search | Read permissions on the fields it reads |
+|------|-----------------|------------------------------------------|
+| **Field selection** — an object listing the fields | Works | Checked per field; a field whose collection the operator cannot read is refused by name |
+| **Handler** — a function returning a condition tree | Returns 403 | Not checked on normal search |
+
+Reach for a field selection whenever the fields are a fixed list. Reach for a handler when the filter depends on the search string itself, on an external service, or on anything else a list cannot express — and accept that extended search stops working on that collection.
+
+
+ **Node.js agent 1.97.3 and later refuse extended search on any collection with a handler.** The agent cannot enumerate which fields a handler reads, so it cannot check them against the operator's `read` permission, and it returns:
+
+ ```
+ You cannot run an extended search on the 'products' collection: the fields it reaches
+ cannot be determined, so they cannot be checked against your permissions.
+ ```
+
+ Normal search keeps working. Converting the handler to a field selection restores extended search, and requires `@forestadmin/datasource-customizer` 1.71.3 or later.
+
+
For large datasets, limit searchable fields to columns with database indexes. Searching unindexed fields causes full table scans.
- In Node.js and Python, the handler receives a `context` with the `generateSearchFilter` helper. In Ruby, the `replace_search` block receives `(search_string, extended_search)` and returns a [condition tree](/get-started/connect/relationships-schema) directly: there is no `generate_search_filter` helper, so you build the tree yourself.
+ The field selection form exists in Node.js only. In Python, the handler receives a `context` with the `generate_search_filter` helper. In Ruby, the `replace_search` block receives `(search_string, extended_search)` and returns a [condition tree](/get-started/connect/relationships-schema) directly: there is no `generate_search_filter` helper, so you build the tree yourself. The Ruby and Python agents do not refuse extended search on a collection with a handler.
### Restricting Which Fields Are Searched
@@ -43,11 +65,8 @@ Use `replaceSearch` in your back-end configuration to define exactly how search
```javascript Node.js / Cloud
agent.customizeCollection('people', collection => {
- collection.replaceSearch((searchString, extendedMode, context) => {
- return context.generateSearchFilter(searchString, {
- extended: extendedMode,
- onlyFields: ['firstName', 'lastName', 'email'],
- });
+ collection.replaceSearch({
+ onlyFields: ['firstName', 'lastName', 'email'],
});
});
```
@@ -98,18 +117,21 @@ agent.customize_collection("people").replace_search(search_in_people)
```javascript
agent.customizeCollection('people', collection => {
- collection.replaceSearch((searchString, extendedMode, context) => {
- return context.generateSearchFilter(searchString, {
- extended: extendedMode,
- excludeFields: ['internalNotes', 'legacyId'],
- });
+ collection.replaceSearch({
+ excludeFields: ['internalNotes', 'legacyId'],
});
});
```
+A field selection also accepts `includeFields`, which adds fields to the default set instead of replacing it. Paths cross relations with a colon, at any depth: `includeFields: ['company:owner:email']`. The agent checks the collection each path ends on — `companies` confers nothing here, `users` needs the `read` permission. A path crossing a `ManyToMany` relation does not resolve and is dropped from the selection without an error; `ManyToOne`, `OneToOne` and `OneToMany` segments all resolve.
+
+
+ A field selection narrows the fields on normal search as well as extended search, so the fields it names are checked against the operator's `read` permission on both. An operator who searches `people` without `read` on a collection an included path ends on receives a 403 where a handler returned results. Grant that permission, or drop the path from the selection.
+
+
### Context-Dependent Search
-Different search logic depending on what the operator is searching for:
+Different search logic depending on what the operator is searching for. This needs a handler, so extended search returns 403 on the collection:
```javascript Node.js / Cloud
@@ -179,9 +201,13 @@ end
```
+
+ In Node.js the `extendedMode` branch of this example never runs from version 1.97.3 on: the agent refuses the extended search before the handler executes. The Ruby example still reaches its `extended_search` branch. To keep an extended search on a collection like this one, split the fixed part of the field list into a field selection and drop the handler, or accept normal search only.
+
+
### Integrating an External Search Engine
-If your data is indexed in Algolia, Elasticsearch, or another service, call it directly in the search handler:
+If your data is indexed in Algolia, Elasticsearch, or another service, call it directly in the search handler. Extended search returns 403 on such a collection in Node.js, since the agent cannot know which columns the external index reads:
```javascript Node.js / Cloud
@@ -272,4 +298,24 @@ agent.customize_collection("products").disable_search()
```
-This is useful for collections where free-text search doesn't apply, for example, collections that only display computed or joined data.
\ No newline at end of file
+This is useful for collections where free-text search doesn't apply, for example, collections that only display computed or joined data.
+
+## Limitations
+
+**A handler gives up extended search in Node.js.** A field selection is the only form the agent can enumerate, so it is the only form that keeps extended search on the collection. A handler whose filter genuinely depends on the search string, on an external index, or on a runtime lookup has no equivalent field selection, and extended search stays refused there. Normal search is unaffected.
+
+**A field selection replaces a datasource's native search.** On a collection whose datasource searches natively — one calling `enableSearch()`, which no Forest-maintained datasource does — a field selection does not narrow that native search: the agent takes the search over and runs its own per-column one on the selected fields. Matching semantics change with it.
+
+**Version requirements.** The refusal starts at `@forestadmin/agent` 1.97.3. The field selection form requires `@forestadmin/datasource-customizer` 1.71.3, shipped in `@forestadmin/agent` 1.98.3.
+
+**Agents differ.** The table below states where each behavior applies today:
+
+| Agent | Field selection form | Extended search with a handler |
+|-------|---------------------|-------------------------------|
+| Node.js, from 1.98.3 | Available | Refused with a 403 |
+| Node.js, 1.97.3 to 1.98.2 | Not available | Refused with a 403 |
+| Node.js, before 1.97.3 | Not available | Served |
+| Ruby | Not available | Served |
+| Python | Not available | Served |
+
+**A handler is exempt from read permissions on normal search.** The operator supplies the text and the handler chooses the fields, so the agent cannot separate a field the customization intended from one the operator's role may not read. A handler pointing at a column of a collection the role cannot read lets that role test values against it, reading each answer from whether rows come back. Prefer a field selection wherever the fields are a fixed list.
diff --git a/reference/agent-api/nodejs.mdx b/reference/agent-api/nodejs.mdx
index 17c3b7f..96caf52 100644
--- a/reference/agent-api/nodejs.mdx
+++ b/reference/agent-api/nodejs.mdx
@@ -914,19 +914,36 @@ collection.addHook('After', 'Update', async (context) => {
### collection.replaceSearch(definition)
-Replace the default search behavior.
+Replace the default search behavior, with a field selection or a handler.
```typescript
collection.replaceSearch(
- definition: SearchDefinition
+ definition: SearchFieldsDefinition | SearchHandlerDefinition
): CollectionCustomizer;
```
-**Example:**
+**Field selection.** Narrows the fields the default search reads. The agent knows which columns the search reads, so it checks them against the caller's `read` permission and extended search keeps working. Requires `@forestadmin/datasource-customizer` 1.71.3.
+
+On a collection whose datasource searches natively — one calling `enableSearch()`, which no Forest-maintained datasource does — a field selection does not narrow that native search: it replaces it with the agent's own per-column search restricted to the selection, so matching semantics change with it.
+
+| Option | Type | Description |
+|--------|------|-------------|
+| `onlyFields` | `string[]` | Replaces the default set with these fields |
+| `includeFields` | `string[]` | Adds these fields to the default set |
+| `excludeFields` | `string[]` | Removes these fields from the default set |
+
+Paths cross relations with a colon, at any depth. `ManyToOne`, `OneToOne` and `OneToMany` segments resolve; a `ManyToMany` segment does not, and a path crossing one is dropped from the selection without an error. `extended` is absent on purpose: the caller owns that flag, and the agent forwards it from the request.
+
+```typescript
+collection.replaceSearch({
+ onlyFields: ['firstName', 'lastName', 'email'],
+});
+```
+
+**Handler.** Receives `(searchString, extended, context)` and returns a condition tree. Use it when the filter depends on the search string, an external service, or anything a field list cannot express.
```typescript
-collection.replaceSearch(async (searchString) => {
- // Search in multiple fields
+collection.replaceSearch(async searchString => {
return {
aggregator: 'Or',
conditions: [
@@ -938,6 +955,10 @@ collection.replaceSearch(async (searchString) => {
});
```
+
+ From `@forestadmin/agent` 1.97.3 on, a handler makes extended search return 403 on the collection: the agent cannot enumerate the fields a handler reads, so it cannot check them against the caller's `read` permission. Normal search is unaffected. See [Search Configuration](/product/build/search-configuration#replacing-the-search-handler).
+
+
---
### collection.disableSearch()