Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/29395-skills-search-auto-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/skills': patch
---

docs(skills): with no `searchableFields` declared, a field marked `searchable: true` joins the default search set

Clause-②: no
Metadata-protocol: none

- **Before.** `objectstack-data`, `objectstack-query` and the `objectstack-ui` list-views rule taught the record-search auto-default as type-only: the name field plus the text-like and `select` columns. A field marked `searchable: true` was described as doing nothing, and a `$searchFields` override naming one as refused.
- **Now.** Each says what the platform resolves: with no `searchableFields` declared, the set `search` scans, and the set a `$searchFields` override may name, is the type-based default plus every field marked `searchable: true`, of any type except `secret`, `password`, `vector` or a virtual `formula`. Each also says what does not change: `searchable: false`, the default, never removes a field the type default scans, and declaring `searchableFields` chooses the set. The enum half of the type default is spelled `select` alone, as the resolver has it.
- No schema, key or behaviour changes.
20 changes: 9 additions & 11 deletions skills/objectstack-data/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ metadata:

## Quick Reference — Detailed Rules

For comprehensive documentation with incorrect/correct examples:
Detailed rules, with incorrect/correct examples:

- **[Naming Conventions](./rules/naming.md)** — snake_case rules, option values, config properties
- **[Field Types](./rules/field-types.md)** — All field types and configs
Expand Down Expand Up @@ -85,7 +85,7 @@ the publish door rejects it before that:
| `highlightFields` | derived | Ordered field keys used as a record's compact face: the columns a **related list** renders on the parent's detail page, and what a lookup picker shows. Declare it on the CHILD object (see [Relationships](./rules/relationships.md)) |
| `namespace` | — | **Not a schema key** — `ObjectSchema.create()` rejects unknown keys, so authoring it is a build error. Embed the prefix directly in `name` instead (e.g. `name: 'crm_account'`) |
| `datasource` | `'default'` | Target datasource ID for virtualized data |
| `nameField` | derived (e.g. `'name'`/`'title'`) | **Canonical** record-title field — the stored field used as the record's display name. Use a single text/email field; a composite title to import, sort or search by is a stored computed field (a non-`formula` field with an `expression`, computed and stored on write); a text formula is display-only |
| `nameField` | derived (e.g. `'name'`/`'title'`) | **Canonical** record-title field — the stored field used as the record's display name. Use a single text/email field; a composite title to import, sort or search by is a stored computed field (a non-`formula` field with an `expression`); a text formula is display-only |
| `displayNameField` | — | **Deprecated** alias for `nameField` (still honored as a fallback) |
| `titleFormat` | — | **Deprecated, not removed (ADR-0079)** — render-only: the server can't return or query it. Use `nameField` (it wins); a composite title follows the `nameField` row |
| `enable` | — | Capability flags (trackHistory, searchable, apiEnabled, etc.) |
Expand Down Expand Up @@ -114,8 +114,10 @@ Toggle system behaviours per object:
`searchableFields` on the object is the canonical set `$search` scans (ADR-0061).
Leave it unset and search auto-defaults to the `nameField` plus the object's
short-text and enum columns (`text` / `email` / `phone` / `url` / `autonumber` /
`textarea` / `markdown` / `select` / `status`); declare it to pin the set
explicitly. Views may narrow it, never widen it.
`textarea` / `markdown` / `select`) plus every field marked `searchable: true`,
of any type except `secret` / `password` / `vector` or a virtual `formula`. `searchable: false`, the
default, never removes a field the type default scans; declare
`searchableFields` to choose the set. Views may narrow it, never widen it.

### ⛔ Searching by a related record's title — mirror the value, always

Expand All @@ -137,14 +139,14 @@ declare *that* field searchable. A task list searched by project name:
fields: {
name: { type: 'text', required: true },
project_id: { type: 'lookup', reference: 'project' },
project_name: { type: 'text', label: 'Project Name' }, // ← the mirror
project_name: { type: 'text', label: 'Project Name' },
},
searchableFields: ['name', 'project_name'],
}
```

`?search=apollo` expands to `name $contains 'apollo' OR project_name $contains
'apollo'` — one single-table scan, every driver, no traversal.
'apollo'` — one single-table scan, every driver.

❌ **Never mirror onto a `formula` field.** It is *virtual* (no column to scan)
and CEL reads only this record's own fields, so it cannot hold the related title;
Expand All @@ -154,15 +156,11 @@ a `formula` entry in any `searchableFields` is refused — `os validate`
**Mirror maintenance is the trade-off** — a mirror is only as fresh as whatever
writes it. Cover both paths: a `beforeInsert` / `beforeUpdate` hook on `task`
stamps `project_name` from the incoming `project_id`'s parent; an `afterUpdate`
hook on `project` re-stamps its tasks when it is renamed. Rows written by a path that bypasses hooks (bulk import, direct SQL) need a
hook on `project` re-stamps its tasks when it is renamed. Rows written past the hooks (bulk import, direct SQL) need a
one-off backfill. See [Lifecycle Hooks](./references/data-hooks.md).

---

→ Moved verbatim to [references/examples-objects.md](./references/examples-objects.md) § Field Groups (MVP).

---

## Conditional Field Rules

Put conditional UI/data-entry rules on the **field definition** when the rule
Expand Down
14 changes: 9 additions & 5 deletions skills/objectstack-query/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,15 @@ to stored values.
protocol normalizes `$searchFields` onto it, and the object form
`search: { query, fields }` spells the same narrowing `fields`.

Omit it to search the object's declared `searchableFields` (or an auto-default
of name/title + short-text fields), resolved server-side. It can only **narrow**
that set, never widen it: over the REST/protocol ingress a name outside it is
`400 INVALID_FIELD`, not a silent fall-back to a full scan. The object form
`search: { query, fields }` stays available for the Tier-2 knobs below.
Omit it to search the object's declared `searchableFields` (or, with none
declared, an auto-default: name/title + short-text fields, plus every field
marked `searchable: true` of any type except `secret` / `password` / `vector` or a virtual `formula`),
resolved server-side. It can only **narrow** that set, never widen it: over the
REST/protocol ingress a name outside it is `400 INVALID_FIELD`, not a silent
fall-back to a full scan. `searchable: false`, the default, never removes a
field the type default already scans; declare `searchableFields` to choose the
set. The object form `search: { query, fields }` stays available for the Tier-2
knobs below.

> ⚠️ **Validates, then silently ignored — never emit these.** `fuzzy`, `boost`,
> `operator`, `minScore`, `language` and `highlight` are the whole set; their
Expand Down
14 changes: 7 additions & 7 deletions skills/objectstack-ui/rules/list-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,10 @@ export const CaseViews = defineView({

| The object … | The allowed set is |
|:-------------|:-------------------|
| declares `searchableFields` | **that list, verbatim** — whatever the field types are |
| declares nothing | the auto-default: the name field + the text-like columns (`text` / `email` / `phone` / `url` / `autonumber` / `textarea` / `markdown` / `select` / `status`) |
| declares `searchableFields` | **that list, verbatim** — any stored field type |
| declares nothing | the auto-default: the name field + the text-like columns (`text` / `email` / `phone` / `url` / `autonumber` / `textarea` / `markdown` / `select`) + every field marked `searchable: true`, of any type except `secret` / `password` / `vector` or a virtual `formula` |

`searchable: false`, the default, removes nothing the type default scans; declare `searchableFields` to choose the set.

Modelling side: **objectstack-data → Search Fields**. Query side
(`search.fields` over the API): **objectstack-query → Full-Text Search**.
Expand All @@ -201,11 +203,9 @@ the whole search box, for every user and every term: no result at all.
|:---------------------------|:--------------|:--------------------------|
| a subset of the allowed set | clean | scans exactly those columns |
| key omitted | clean | scans the object's full allowed set |
| `searchableFields: []` | clean | **identical to omitting it** — empty is *absent* at all three layers, so it scans MORE than `["subject"]` would |
| a renamed / mistyped column | `searchable-field-unknown` | `400 INVALID_FIELD` |
| a dotted path (`account_id.name`) | `searchable-field-unknown` | `400 INVALID_FIELD` |
| a real column outside the allowed set | `searchable-field-unsearchable` | `400 INVALID_FIELD` |
| a virtual `formula` column — nothing stored to scan | `searchable-field-unsearchable` | `400 INVALID_FIELD` |
| `searchableFields: []` | clean | **identical to omitting it** — empty is *absent*, so it scans MORE than `["subject"]` would |
| a renamed / mistyped column, or a dotted path (`account_id.name`) | `searchable-field-unknown` | `400 INVALID_FIELD` |
| a real column outside the allowed set, or a virtual `formula` column | `searchable-field-unsearchable` | `400 INVALID_FIELD` |

Both diagnostics are **errors**, not warnings — `os validate` fails the build.

Expand Down
Loading