Skip to content

Commit 385aa12

Browse files
fix(spec,service-automation)!: QuerySchema.fields and the get_record node's fields refuse an empty projection (#27244)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB8Kh2HdiJzzy5dQUrvvTRJa)_ Fixes #27166 Clause-②: no (narrowing) Metadata-protocol: narrowed Skills-docs: none — `skills/**` teaches `fields` and nested `expand` `fields` only as non-empty lists (`skills/objectstack-query/SKILL.md`: the nested `expand` value row and the nested-expand example), and teaches the `get_record` node without `fields` (`skills/objectstack-automation/SKILL.md`); no file teaches an empty projection, and every taught use stays true. ## 维护者速读 - **改了什么**:最后两份还接受空投影的公开声明——QueryAST 自身的 `QuerySchema.fields`(因而也是每一层 `expand` 条目的值类型)和流程 `get_record` 节点的 `fields`——改为与其余投影声明共用同一条规则:至少一个字段名,且不能为空白。报错文字同一处定义,不复制。 - **为什么改**:#27161 之后引擎在 `find` / `findOne` 入口(顶层与任意层 `expand`)拒绝空投影,而这两份声明仍写着「空也收」:`get_record` 节点写 `fields: []` 能保存,运行时才失败。现在保存时就被拒。 - **风险与回滚**:低。仓内没有任何非测试的作者写这两种空投影;不写 `fields` 仍读全部字段,非空列表不变,类型不变(`string[]`)。附带一处可见变化:`POST /data/:object/query` 请求体里 `fields: []` 由请求体 schema 拒绝(`400 VALIDATION_FAILED`,`query.fields`),与其别名 `$select: []` 的回答一致;此前由 normalizer 拒绝(`400 VALIDATION_ERROR`)。两者都是拒绝,只是错误码变了。回滚即 revert。 - **先看**:`packages/spec/src/data/projection-names-a-field.ts`(共享规则移入的新模块)、`query.zod.ts` 与 `builtin-node-config.zod.ts` 各一行、`packages/spec/src/data/projection-names-a-field.test.ts`。 - **两分钟验证**:`pnpm --filter @objectstack/spec exec vitest run src/data/projection-names-a-field.test.ts`,预期 28/28 通过;把 `query.zod.ts` 的 `fields` 改回 `z.array(FieldNodeSchema)` 时 15 条失败。 - **需要你做的**:无,席位落地。 ## What changes | Shape | `main` (9dacccf) | This head | |---|---|---| | `QuerySchema.parse({ object, fields: [] })` | parses | refused at `fields`: "Name at least one field" | | `QuerySchema` with `expand: { rel: { object, fields: [] } }`, any depth | parses | refused at `expand.rel.fields` (or deeper), same words | | `EngineQueryOptionsSchema` with an `expand` entry's `fields: []` | parses | refused at `expand.rel.fields`, same words | | `fields: ['name', '']` / `['name', ' ']` on any of the above | parses | refused at `fields.1`: "A field name cannot be blank" | | `QueryWithTransportSchema` canonical `fields: []` | parses (its `$select: []` alias was already refused) | refused at `fields`, the same message `$select: []` gets | | `POST /data/:object/query` body `{ "fields": [] }` | `400 VALIDATION_ERROR` from the normalizer (on 18.2.0: `200`, every column) | `400 VALIDATION_FAILED` at `query.fields` from the body schema, as `$select` / `select` | | `POST /data/:object/query` body `{ "expand": { "owner": { "object": "sys_user", "fields": [] } } }` | `400 VALIDATION_ERROR` from the engine door (on 18.2.0: `200`, every column of the related record) | `400 VALIDATION_FAILED` at `query.expand.owner.fields` from the body schema | | `GetRecordConfigSchema` `fields: []` | parses | refused at `fields`, same words | | a flow `get_record` node with `fields: []` | saves; run fails with the engine's refusal | `defineStack` refuses it with `422 STACK_SCHEMA_INVALID` at `flows.0.nodes.1.config.fields`; the save door's node judge (`flowNodeConfigRefusals`) refuses it at `fields`; the executor refuses the config before any read | | `fields` absent, or a non-empty list | parses | unchanged | - **One declaration, not a copy.** `PROJECTION_NAMES_NO_FIELD` and `transportProjectionArray` move from `data-engine.zod.ts` into the new `packages/spec/src/data/projection-names-a-field.ts`. They had to move: `data-engine.zod.ts` imports `query.zod.ts`, so `query.zod.ts` could not read them where they were. The module stays outside the `data` barrel, so no public export is added (as with `filter-save-door-refusals.ts`). `data-engine.zod.ts` imports both names and keeps its string arm and its docblock; the docblock gains one paragraph that says where the array arm now lives and why. - `BaseQuerySchema.fields` becomes `transportProjectionArray(FieldNodeSchema)`, the construction `EngineQueryOptionsSchema.fields` and `QueryTransportParamsSchema.$select` already use. `QuerySchema` is one declaration, so this also covers its top level (the driver tuple inputs, the API contract input, the canonical `fields` of `QueryWithTransportSchema`) and every nested `expand` value. Seat 1 ruled for narrowing the base declaration rather than an expand-only copy of the recursive schema. - `GetRecordConfigSchema.fields` becomes `transportProjectionArray(z.string())`. - Both `.describe()` texts now say that an empty list or a blank name is refused and that omitting `fields` reads every field. The five reference pages that render them are regenerated (`gen:docs`). - `@objectstack/service-automation`: the `get_record` designer form declares `fields` with `minItems: 1`, the way its `limit` mirrors the contract's floor, so the form no longer offers an empty list the save refuses. `builtin-node-form-zod-ledger.test.ts` pins both sides at that boundary, beside the `limit` pin. - ADR-0087: `registered query-and-get-record-empty-projection-refused`, a new step-18 D3 semantic entry. There is no D2 conversion: the only mechanical rewrite of `fields: []` is to drop the key, which reads every column, and that is the widening the rule refuses. Stored flows are not rewritten. - Changeset `.changeset/27166-query-get-record-empty-projection.md`: `@objectstack/spec` minor and `@objectstack/service-automation` minor (`Metadata-protocol: narrowed` takes `minor` on each package whose `src/**` the diff moves), with both declaration lines, the `Skills-docs:` line, the ADR-0087 marker and a BREAKING banner with Before / Now / Unchanged and the remedy. - `content/docs/data-modeling/queries.mdx`: the empty-projection sentence names each refusal point (`4dbe7883`). A POST body is refused by the schema (`VALIDATION_FAILED` at `query.fields`); the querystring forms (`?fields=`, `$select=`) keep `VALIDATION_ERROR`; `engine.find` in-process refuses it; a `get_record` node is refused when the flow is saved. - **The changeset is anchored to `@objectstack/spec@18.2.0`.** Its **Before** now describes the last release: there, a `get_record` node with `fields: []` saved, registered and ran, reading every column, and a REST POST with `fields: []` answered `200`. #26644 and #27008 are in the same unreleased batch. The changeset also names the stored-flow consequence: such a flow is refused by `registerFlow` on every pull, and it stays unregistered with its triggers unbound until the row is edited. `os migrate meta --stored` has no conversion to apply to it. It also carries a **Supersedes, in the same release:** paragraph for the `26954-engine-projection-empty.md` sentence that says `engine.find(object, { fields: [] })` still reads every column, which #27008 made false; the precedent is `25923-calendar-gantt-flat-fallback-retired.md`. The D3 entry's `reason` and `acceptanceCriteria` say the same. - `data-empty-projection-count-door.test.ts` gains the `expand` row: the test checks for `400 VALIDATION_FAILED` and `fields[0].field = query.expand.owner.fields`, and that the object was never read. ## Why #26954 (PR #27007) made the engine-side projection declarations refuse `[]`. #27008 (PR #27161) made the in-process engine refuse it at the `find` / `findOne` entry, top level and on every `expand` entry. Its acceptance notes reported two declarations still admitting it: the QueryAST's `fields`, which is the value type of `EngineQueryOptionsSchema.expand`, and `GetRecordConfigSchema.fields`, which the `get_record` executor hands to `data.find` / `findOne` as written. So the runtime refused shapes these declarations admitted, and a `get_record` node with `fields: []` was caught at run instead of at save. Triage 6466002857 set the lane to `domain:spec`. ## Readers and producers (measured on `origin/main` 9dacccf) - **Who parses these shapes at run time.** - The `get_record` executor parses its config with `GetRecordConfigSchema` (`crud-nodes.ts`, `parseNodeConfig`). - `defineStack` / `os validate` and `registerFlow` judge every builtin node config through `flowNodeConfigRefusals`. - The REST body schema (`FindDataRequestSchema.query` = `QueryWithTransportSchema`) extends `QuerySchema`'s shape, so its canonical `fields` narrows too. - No code outside `packages/spec` parses `QuerySchema` itself, nor the driver tuple inputs. - **Census of authored empty projections on the two shapes.** I ran `git grep` over `packages`, `ui`, `examples`, `apps` and `skills`. - One hit: `get-record-id-projection.integration.test.ts:167`, the #27161 test that pinned the run-time refusal. It now pins the config refusal: the engine is never asked, and the node fails with `config.fields: Name at least one field.` - Two other tests pinned the old acceptance and are rewritten. `query.test.ts` ("should handle empty arrays") drops `fields: []` and points at the new file. `data-empty-projection-count-door.test.ts` moves its `fields: []` row from the normalizer table to the body-schema table and adds a blank-member row. - No `examples/**` or non-test source authors either shape empty. - **The designer UI.** `ui/` offers no `fields` input for `get_record` (`flow-node-config.ts` has `objectName`, `filter`, `limit`, `outputVariable`), so no `ui/` change is needed. - **Types.** `z.input` / `z.infer` stay `string[]`. `typecheck` exits 0 for spec, service-automation and rest. - **JSON Schema.** `fields` gains `minItems: 1` and a non-blank `pattern` on each item. `check:generated` reported only the reference pages stale, and they are regenerated. ## Verification All at head `939accc9` unless a line says otherwise. **Round 2, on the merged tree (`a1b4b045`, merge `295699d5`)**, all exit 0: `check:generated`; spec `src/data` + `src/automation` + `src/migrations` (189 files, 6392 passed); spec typecheck; service-automation `src/builtin` (82 files, 1154 passed); the three rest files (120 passed); `check-adr-0087-registration`, `check-changeset-no-major`, `check-empty-changeset`. <details><summary>New pins and ablation</summary> `packages/spec/src/data/projection-names-a-field.test.ts` has 28 cases: - `QuerySchema` refuses `[]`, `['name', '']` and `['name', ' ']` at the top level, on an `expand` entry and on a second-level `expand` entry. Each refusal asserts the path and the message `EngineQueryOptionsSchema.fields` gives for the same value. - `EngineQueryOptionsSchema.expand` refuses the same values on an entry. - `QueryWithTransportSchema`'s canonical `fields` refuses them with `$select`'s message. - `GetRecordConfigSchema` refuses them. The save door's node judge refuses them at `fields` / `fields[1]`. `defineStack` refuses a flow whose `get_record` node carries `fields: []` with `422 STACK_SCHEMA_INVALID` at `flows.0.nodes.1.config.fields`. - Controls: one or two names parse unchanged, and absent `fields` parses unchanged, on every shape. Ablation after commit, through `scripts/ablation-replace.mjs` (anchor hit 1 → 0; restored to the HEAD blob with `git diff HEAD` empty): - `query.zod.ts` `fields` reverted to `z.array(FieldNodeSchema)`: 15 failed / 13 passed. These are the five `QuerySchema` / `expand` / transport groups. - `builtin-node-config.zod.ts` `fields` reverted to `z.array(z.string())`: 7 failed / 21 passed. These are the three contract refusals, the three node-judge refusals and the `defineStack` case. </details> <details><summary>Local runs</summary> - `pnpm --filter @objectstack/spec build && check:generated`: only `content/docs/references/**` was stale. `gen:docs` was run and the result committed. - `@objectstack/spec` full suite, at `bd35a3fa` under the lock: 823 files, 23598 passed, 1 failed (`query.test.ts` "should handle empty arrays", the old acceptance pin, rewritten in the next commit; that file is then 91/91). `typecheck`: exit 0. - `@objectstack/service-automation` build, suite and typecheck, at `bd35a3fa`: 216 files, 2756 passed, 1 failed (the rewritten get_record pin, whose expected path read `fields:` where the executor prints `config.fields:`; fixed in the next commit, and that file plus the form ledger are then 17/17). `typecheck`: exit 0. - `@objectstack/rest` suite: 338 files passed, 6835 tests passed, 344 skipped. - `@objectstack/metadata-protocol` suite: 277 files passed, 4 skipped; 21750 tests passed, 26 skipped. - Full dogfood, owed by the `(narrowing)` arm, after the `@objectstack/dogfood^...` closure build: `pnpm --filter @objectstack/dogfood test` under the lock, no shard, no path: Test Files 348 passed | 3 skipped (351); Tests 2804 passed | 20 skipped (2824); exit 0. </details> <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 109 commands, all run serially at this head. 107 exited 0 on the first pass, including `check-adr-0087-registration --base origin/main` (`registered query-and-get-record-empty-projection-refused (new here)`), `check-changeset-no-major --base origin/main` and `spec check:generated` (14/14 up to date). Two exited 3 because their prerequisite was not met: `spec check:skill-examples` and `check:dual-build-cjs-loads`. They read built output. After I built the seven packages whose `dist` they named, both exited 0. `--ran` reconciliation: "109 derived famil(ies) accounted for — 107 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)", and those two then ran green. `check-plugin-teardown-shape --self-test` ran after fetching its pinned fixture commit. Also `eslint --no-inline-config` on the 11 changed `.ts` files: exit 0. `@objectstack/rest` `typecheck`: exit 0. </details> ## Acceptance notes - **The REST code change is deliberate and is the ruling's consequence.** A `POST /data/:object/query` body with `fields: []` was refused before and is refused now. The code moves from `VALIDATION_ERROR` (normalizer) to `VALIDATION_FAILED` (body schema), and the refusal now names `query.fields`, as it already did for `select` / `$select`. The querystring spellings (`?fields=`, `?$select=`) are unchanged: they go through the normalizer as before. - **The in-process engine already refused these shapes** (#27161). This diff changes no runtime source except the designer form's `minItems`. - **`main` was merged (merge commit, no rebase) at `295699d5`.** Main had moved `content/docs/references/api/protocol.mdx` and `packages/spec/src/api/protocol.zod.ts`, in hunks that do not overlap this diff. `protocol.mdx` is a generated artifact, so it was regenerated on the merged tree (`gen:schema && gen:docs`) in its own commit, not text-merged. After the merge, the PR's delta against `main` is 19 files: the 18 above, plus `packages/runtime/src/domains/data-empty-projection-count.test.ts` (`c483a00e`). The runtime dispatcher's door moves the same way as REST. Its POST body `{ "fields": [] }` row moves from the normalizer table to the file's schema table and expects `VALIDATION_FAILED` at `query.fields`, next to the blank-member and `expand` rows, mirroring the rest pin. That row had turned Test Core 3/8 red on `4dbe7883`. - **Stored flows are not rewritten.** A stored flow whose `get_record` node carries `fields: []` already failed at run on `main`. It now also meets the refusal where a flow is registered or saved. The D3 entry tells authors to sweep for it. ## Human Minimum Each item is one rule from [CONTRIBUTING.md › Human Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum), which links the AGENTS.md text that states it. - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [ ] 3. No formatter ran; `pnpm lint` passes. (No formatter ran. `eslint --no-inline-config` over every changed `.ts` file exits 0; the repo-wide `pnpm lint` is CI's.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/` (the reference pages are `gen:docs` output). - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzfMG4xAptDoRoz9kyT)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3beb224 commit 385aa12

19 files changed

Lines changed: 335 additions & 51 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
---
5+
6+
fix(spec)!: `QuerySchema.fields` (and so every nested `expand` entry) and the flow `get_record` node's `fields` refuse an empty projection, as every other published projection declaration and the in-process engine do (#27166)
7+
8+
Clause-②: no (narrowing) — two published declarations of a read's projection, `QuerySchema.fields` (the QueryAST, which is also the value type of every `expand` entry, `EngineQueryOptionsSchema.expand` included, and the canonical `fields` of the transport-aware query slot) and `GetRecordConfigSchema.fields` (the flow `get_record` node), refuse a list naming no field, which they admitted before. Nothing is widened and no key is added, removed or renamed.
9+
Metadata-protocol: narrowed
10+
Skills-docs: none — `skills/**` teaches `fields` and nested `expand` `fields` only as non-empty lists (`skills/objectstack-query/SKILL.md`, the `expand` value row and the nested-expand example), and teaches the `get_record` node without `fields` (`skills/objectstack-automation/SKILL.md`); no file teaches an empty projection, and every taught use stays true.
11+
12+
<!-- adr-0087: registered query-and-get-record-empty-projection-refused -->
13+
14+
**BREAKING** for a flow `get_record` node, or a query you build, that carries an empty projection. In this same release the in-process engine starts refusing `fields: []` at its `find` / `findOne` entry, top level and at any `expand` depth; without this change the two declarations below would still admit it, so such a `get_record` node would save clean and fail at its first run.
15+
16+
- **Before (`@objectstack/spec@18.2.0`, the last release).** `QuerySchema.fields` and `GetRecordConfigSchema.fields` admitted any list of strings: `[]`, and a list with a blank or whitespace-only name. A `get_record` node with `fields: []` passed `os validate`, `defineStack` and `registerFlow`, and its run read every column of the row. A `POST /data/:object/query` body with `fields: []`, top level or on an `expand` entry, answered `200` with every column.
17+
- **Now.** A list must hold at least one name, and no name may be blank (JSON Schema `minItems: 1`, and a non-blank pattern on each name). Both read the rule from the declaration `EngineQueryOptionsSchema.fields` and `QueryTransportParamsSchema.$select` read, so the refusal says the same thing: "Name at least one field", or "A field name cannot be blank" for a blank member. A `get_record` node with `fields: []` is refused where a flow is saved or registered, at `flows.N.nodes.M.config.fields` (`STACK_SCHEMA_INVALID` from `defineStack`), and its executor refuses the config before any read. An `expand` entry with `fields: []` is refused at `expand.<field>.fields`. A `POST /data/:object/query` body with `fields: []` is refused by the body schema, `400 VALIDATION_FAILED` at `query.fields`, which is the answer its `$select: []` spelling gets; an `expand` entry's `fields: []` in that body is refused the same way, at `query.expand.<field>.fields`.
18+
- **`@objectstack/service-automation`.** The `get_record` node's designer form (`configSchema`) declares `fields` with `minItems: 1`, as its `limit` already mirrors the contract's floor, so the form no longer offers an empty list the save refuses.
19+
- **Stored flows.** They are not rewritten, and there is no conversion for `os migrate meta --stored` to apply. A stored flow whose `get_record` node carries `fields: []` is refused by `registerFlow` on every pull of the stored definitions, so it stays unregistered, and its triggers unbound, until the row is edited: name the fields, or remove the key.
20+
- **Unchanged.** The inferred types (`string[]`), a named projection, and leaving `fields` out to read every column.
21+
22+
**Supersedes, in the same release:** `26954-engine-projection-empty.md` says, under **Unchanged**, that the in-process ObjectQL engine does not parse its options with that schema and that `engine.find(object, { fields: [] })` still reads every column, as before. The engine change in this release (`27008-engine-empty-projection-refused.md`) refuses that call at its `find` / `findOne` entry, so that sentence no longer holds: an empty projection is refused by the declarations and by the engine alike, and leaving `fields` out is the one way to read every column.
23+
24+
**The remedy.** Name the fields to read, or remove `fields` to read every field; on an `expand` entry, the same for the related record. There is no mechanical rewrite: dropping the key reads every column, which is the widening the rule refuses, so the choice is the author's.

‎content/docs/data-modeling/queries.mdx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,13 @@ retired, read them with the operator-only
296296
them before `os migrate apply --allow-destructive` drops the column. No runtime door serves
297297
them.
298298

299-
`fields` names at least one column. An empty list (`fields: []`, `$select=`) is refused with
300-
`400 VALIDATION_ERROR` — on the data API, and by `engine.find()` / `findOne()` for an in-process
301-
caller (a plugin, a hook, a flow), the top-level `fields` and an `expand` entry's alike — rather
302-
than read as "every column". Leave `fields` out to read every column.
299+
`fields` names at least one column. An empty list, or a list with a blank name, is refused
300+
rather than read as "every column", the top-level `fields` and an `expand` entry's alike: by the
301+
query schema itself (`QuerySchema`), so a `POST /data/:object/query` body with `fields: []` answers
302+
`400 VALIDATION_FAILED` naming `query.fields`; on the querystring (`?fields=`, `$select=`) with
303+
`400 VALIDATION_ERROR`; and by `engine.find()` / `findOne()` for an in-process caller (a plugin, a
304+
hook). A flow `get_record` node with `fields: []` is refused when the flow is saved. Leave
305+
`fields` out to read every column.
303306

304307
### Nested / Related Fields
305308

‎content/docs/references/api/contract.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ const result = ApiErrorSchema.parse(data);
580580
| Property | Type | Required | Description |
581581
| :--- | :--- | :--- | :--- |
582582
| **object** | `string` | ✅ | Object name (e.g. account) |
583-
| **fields** | `string[]` | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (`owner.name`) is not a projection: no driver resolves one, and the ingress refuses it with `400 INVALID_FIELD`. Related data is read with `expand`, whose nested QueryAST both filters (`where`) and selects (`fields`) the related record's columns. The projection must RETAIN the foreign-key column: `fields: ['title']` with `expand: 'project_id'` resolves nothing, because the relation is carried by that key — add `'project_id'` and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
583+
| **fields** | `string[]` | optional | Fields to retrieve — names of the queried object's OWN columns, at least one: omit `fields` to read every column, because an empty list or a blank name is refused rather than read as every column. A dotted path (`owner.name`) is not a projection: no driver resolves one, and the ingress refuses it with `400 INVALID_FIELD`. Related data is read with `expand`, whose nested QueryAST both filters (`where`) and selects (`fields`) the related record's columns. The projection must RETAIN the foreign-key column: `fields: ['title']` with `expand: 'project_id'` resolves nothing, because the relation is carried by that key — add `'project_id'` and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
584584
| **where** | `any` | optional | Filtering criteria (WHERE) |
585585
| **search** | `string \| { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' \| 'or'>; … }` | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
586586
| **searchFields** | `string[]` | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
@@ -633,7 +633,7 @@ const result = ApiErrorSchema.parse(data);
633633
| Property | Type | Required | Description |
634634
| :--- | :--- | :--- | :--- |
635635
| **object** | `string` | ✅ | Object name (e.g. account) |
636-
| **fields** | `string[]` | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (`owner.name`) is not a projection: no driver resolves one, and the ingress refuses it with `400 INVALID_FIELD`. Related data is read with `expand`, whose nested QueryAST both filters (`where`) and selects (`fields`) the related record's columns. The projection must RETAIN the foreign-key column: `fields: ['title']` with `expand: 'project_id'` resolves nothing, because the relation is carried by that key — add `'project_id'` and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
636+
| **fields** | `string[]` | optional | Fields to retrieve — names of the queried object's OWN columns, at least one: omit `fields` to read every column, because an empty list or a blank name is refused rather than read as every column. A dotted path (`owner.name`) is not a projection: no driver resolves one, and the ingress refuses it with `400 INVALID_FIELD`. Related data is read with `expand`, whose nested QueryAST both filters (`where`) and selects (`fields`) the related record's columns. The projection must RETAIN the foreign-key column: `fields: ['title']` with `expand: 'project_id'` resolves nothing, because the relation is carried by that key — add `'project_id'` and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
637637
| **where** | `any` | optional | Filtering criteria (WHERE) |
638638
| **search** | `string \| { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' \| 'or'>; … }` | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
639639
| **searchFields** | `string[]` | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |

‎content/docs/references/api/protocol.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,7 @@ Enable package response
899899
| Property | Type | Required | Description |
900900
| :--- | :--- | :--- | :--- |
901901
| **object** | `string` | ✅ | Object name (e.g. account) |
902-
| **fields** | `string[]` | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (`owner.name`) is not a projection: no driver resolves one, and the ingress refuses it with `400 INVALID_FIELD`. Related data is read with `expand`, whose nested QueryAST both filters (`where`) and selects (`fields`) the related record's columns. The projection must RETAIN the foreign-key column: `fields: ['title']` with `expand: 'project_id'` resolves nothing, because the relation is carried by that key — add `'project_id'` and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
902+
| **fields** | `string[]` | optional | Fields to retrieve — names of the queried object's OWN columns, at least one: omit `fields` to read every column, because an empty list or a blank name is refused rather than read as every column. A dotted path (`owner.name`) is not a projection: no driver resolves one, and the ingress refuses it with `400 INVALID_FIELD`. Related data is read with `expand`, whose nested QueryAST both filters (`where`) and selects (`fields`) the related record's columns. The projection must RETAIN the foreign-key column: `fields: ['title']` with `expand: 'project_id'` resolves nothing, because the relation is carried by that key — add `'project_id'` and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
903903
| **where** | `[string, string, any] \| [string, string] \| [string, object, ...object[]] \| object[] \| Record<string, any> \| any` | optional | Filtering criteria (WHERE) — a filter condition, or the input-only `FilterArray` sugar (`['status', '=', 'open']`), which is lowered through `parseFilterAST` before the query is produced. |
904904
| **search** | `string \| { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' \| 'or'>; … }` | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
905905
| **searchFields** | `string[]` | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |

‎content/docs/references/automation/builtin-node-config.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
232232
| :--- | :--- | :--- | :--- |
233233
| **objectName** | `string` | ✅ | Object to query |
234234
| **filter** | `Record<string, any>` | optional | Field/value pairs to match: each operand — a field's value, an operator's comparand (`{"$ne": null}`), a list comparand's element — is a CEL value envelope `{ dialect: 'cel', source }` evaluated before the query (one that evaluates to null refuses the node), a query placeholder such as `{current_user_id}` or `{today}` the query engine resolves, or a literal; a flow `{…}` template token is refused |
235-
| **fields** | `string[]` | optional | Field projection — only these fields are read (default: all) |
235+
| **fields** | `string[]` | optional | Field projection — only these fields are read; omit it to read all. An empty list or a blank name is refused |
236236
| **limit** | `integer` | optional | Max records to return — a positive integer; >1 switches to a multi-record query |
237237
| **outputVariable** | `string` | optional | Flow variable the result is bound to — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
238238

0 commit comments

Comments
 (0)