You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit fd51a66
Browse filesBrowse the repository at this point in the historyBrowse files
fix(rest)!: an async import job for an unregistered object is refused with 404 `OBJECT_NOT_FOUND` before any job is created (#27501)
6
+
7
+
Clause-②: no (narrowing) — `POST /data/:object/import/jobs` now refuses an object name the runtime does not register, where it used to answer `201` and persist a job. Nothing is widened.
8
+
Metadata-protocol: none
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: one REST door now refuses, up front, an object name that every row of its job was already refused for. There is nothing for a migration to rewrite and no tombstone. -->
11
+
12
+
**BREAKING** accept-set narrowing at one REST data door (`@objectstack/rest`).
13
+
14
+
-**Before.**`POST /data/no_such_object/import/jobs` answered `201` with a job id and persisted a `sys_import_job` row. The job then ran, every row failed with `OBJECT_NOT_FOUND`, and the job read `succeeded`.
15
+
-**Now.** The door answers `404` with `error.code: 'OBJECT_NOT_FOUND'`, the same envelope `POST /data/no_such_object` answers, before the body is parsed. No job row is written and no worker runs.
16
+
-**Unchanged.** A registered object gets `201` and a job exactly as before. A job whose rows partly fail still ends `succeeded`, as before.
17
+
-**Who sees it.** An import client that sends an object name nothing is behind, for example a typo. Such a request used to look accepted while it could import nothing.
18
+
-**Also refused up front.** An object of a disabled package now gets the data door's `404 OBJECT_PACKAGE_DISABLED` instead of a job (re-enable the package). A legacy `ns__short` object addressed by its short name alone gets `404 OBJECT_NOT_FOUND`, because the door counts an object only under its own name, as the API-exposure gate does (send the full registered name).
19
+
20
+
**The remedy.** Send the object's registered name, the one `POST /data/:object` accepts.
fix(spec): the generated protocol upgrade guide keeps an entry's `surface` in one code span when the surface carries backticks of its own
6
+
7
+
Clause-②: no (a generator fix: no schema, accepted value, served body or error code moves; only the rendering of the guide the package ships changes)
8
+
Metadata-protocol: none
9
+
10
+
`protocol-upgrade-guide.md`, generated at publish from the ADR-0087 registries, shipped in `@objectstack/spec` and attached to the GitHub Release, wrapped every conversion's and semantic entry's `surface` in a single-backtick code span. Most semantic entries mark their own inline code inside the surface, and CommonMark closes a single-backtick span at the first backtick inside it, so those lines rendered as alternating code and prose fragments. The generator now fences each surface by CommonMark's rule, one backtick longer than the longest run inside it, so a surface renders as one span with the author's backticks shown literally; a surface with no backtick and no leading or trailing space is byte-identical to before. A conversion-table cell also escapes `|`, the one character a table row reads as structure.
fix(spec)!: `QueryTransportParamsSchema.$searchFields`, `QuerySchema.searchFields` and `FullTextSearchSchema.fields` declare the search-field override the Data API doors accept, instead of any string or list (#27683)
6
+
7
+
Clause-②: no (narrowing) — the published declarations of the search-field override (`$searchFields`, the canonical `searchFields`, and the structured `search.fields`, reached through `QueryWithTransportSchema` and `FindDataRequestSchema.query`) refuse a value naming no field, which they admitted before. The doors already refused it with a 400; nothing is widened and no key is added, removed or renamed.
8
+
Metadata-protocol: none
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and no metadata type or stored shape moves: these are query declarations no metadata type embeds, never stored, and the values they stop admitting are values the Data API doors already refused. A caller names at least one field or leaves the key out; there is nothing for a migration to rewrite and no tombstone. -->
11
+
12
+
**BREAKING** accept-set narrowing of published query declarations (`@objectstack/spec/data`, and `@objectstack/spec/api` through `FindDataRequestSchema`), read by the `POST /data/:object/query` body gate, codegen, the reference docs and clients.
13
+
14
+
-**Before.**`$searchFields` admitted any string or list: a blank, whitespace-only or comma-only string, and `[]`. `QuerySchema.searchFields` and `FullTextSearchSchema.fields` admitted `[]`. The published JSON Schema admitted what the doors refuse.
15
+
-**Now.** A string must name at least one field after the door's own split and trim (published as the pattern `[^\s,]`), and a list must hold at least one name. That is exactly the set the door refuses as naming no field. A list with a blank member is still admitted by the schema, as the door admits it as a value and then refuses the blank name as an unknown field.
16
+
-**`POST /data/:object/query`.** That route parses its body with `FindDataRequestSchema`, so an empty `searchFields`, `$searchFields` or structured `search.fields` is now refused at the body schema: still `400`, still before any read, but `VALIDATION_FAILED` with `fields[0].field` naming `query.searchFields`, `query.$searchFields` or `query.search.fields`, where it was `VALIDATION_ERROR` from the list door. This is how that route already answers an empty `select`.
17
+
-**Unchanged.**`GET /data/:object` and `GET /data/:object/export` still answer an empty `$searchFields` / `searchFields` with `400 VALIDATION_ERROR` naming it; a named override (`label`, `["label", "note"]`, spaces and empty members between commas trimmed by the door) and an explicit `null` (a withdrawal) are accepted as before. The engine-side declarations (`EngineQueryOptionsSchema.searchFields` and its aggregate and count siblings) still admit `[]`, and the in-process engine still reads an empty override as absent; moving them is a separate change. The structured `search` form those engine declarations share is `FullTextSearchSchema`, so its `fields: []` is refused there too, though the engine itself does not parse `search`.
18
+
-**Who sees it.** A caller of `POST /data/:object/query` that branches on the `VALIDATION_ERROR` code for an empty search-field override, code that parses a query bag with these schemas and sent an empty override, or a generated client that trusted the wider declaration.
19
+
20
+
**Supersedes, in the same release.** The `#27484` entry's "Not yet narrowed" bullet no longer holds: the three declarations it names other than `EngineQueryOptionsSchema.searchFields` refuse `''` and `[]` now, and on `POST /data/:object/query` an empty `searchFields` is the body schema's `400 VALIDATION_FAILED`, the same answer `select: []` gets.
21
+
22
+
**The remedy.** Send the override naming at least one field, or leave it out to search the object's default search fields.
Copy file name to clipboardExpand all lines: content/docs/references/api/contract.mdx
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -587,7 +587,7 @@ const result = ApiErrorSchema.parse(data);
587
587
|**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. |
|**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 |
590
-
|**searchFields**|`string[]`| optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
590
+
|**searchFields**|`string[]`| optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) — at least one field name; a value naming no field (blank, only commas and spaces, or an empty list) is refused |
|**limit**|`number`| optional | Max records to return (LIMIT) |
593
593
|**offset**|`number`| optional | Records to skip (OFFSET) |
@@ -606,7 +606,7 @@ const result = ApiErrorSchema.parse(data);
606
606
| Property | Type | Required | Description |
607
607
| :--- | :--- | :--- | :--- |
608
608
|**query**|`string`| ✅ | Search query text |
609
-
|**fields**|`string[]`| optional | Fields to search in (if not specified, searches all text fields) |
609
+
|**fields**|`string[]`| optional | Fields to search in (if not specified, searches all text fields) — at least one field name; a value naming no field (blank, only commas and spaces, or an empty list) is refused |
610
610
|**fuzzy**|`boolean`| optional (default: `false`) |[EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only `query` + `fields`; no executor receives this flag. |
611
611
|**operator**|`Enum<'and' \| 'or'>`| optional (default: `"or"`) |[EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag. |
612
612
|**boost**|`Record<string, number>`| optional |[EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results. |
@@ -640,7 +640,7 @@ const result = ApiErrorSchema.parse(data);
640
640
|**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. |
|**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 |
643
-
|**searchFields**|`string[]`| optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
643
+
|**searchFields**|`string[]`| optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) — at least one field name; a value naming no field (blank, only commas and spaces, or an empty list) is refused |
Copy file name to clipboardExpand all lines: content/docs/references/api/protocol.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -902,7 +902,7 @@ Enable package response
902
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. |
903
903
|**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. |
904
904
|**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 |
905
-
|**searchFields**|`string[]`| optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
905
+
|**searchFields**|`string[]`| optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) — at least one field name; a value naming no field (blank, only commas and spaces, or an empty list) is refused |
|**limit**|`integer`| optional | Max records to return (LIMIT) |
908
908
|**offset**|`integer`| optional | Records to skip (OFFSET) |
@@ -923,7 +923,7 @@ Enable package response
923
923
|**$select**|`string \| string[] \| null`| optional | Transport spelling of `fields` (OData `$select`) — at least one field name; a value naming no field (blank, only commas and spaces, an empty list, or a list with a blank name) is refused |
|**$searchFields**|`string \| string[] \| null`| optional | Transport spelling of `searchFields` (OData `$searchFields`) |
926
+
|**$searchFields**|`string \| string[] \| null`| optional | Transport spelling of `searchFields` (OData `$searchFields`) — at least one field name; a value naming no field (blank, only commas and spaces, or an empty list) is refused |
927
927
|**$count**|`boolean \| Enum<'true' \| 'false'> \| null`| optional | Transport spelling of the response total-count flag (OData `$count`) |
928
928
|**count**|`boolean \| Enum<'true' \| 'false'> \| null`| optional | Response total-count flag — only an explicit `false` skips the COUNT query |
929
929
|**filter**|`[string, string, any] \| [string, string] \| [string, object, ...object[]] \| object[] \| Record<string, any> \| any \| null`| optional | Transport spelling of `where`|
0 commit comments