Skip to content

Commit 4ac386d

Browse files
committed
docs(state): document nested append paths and Case-2 behavior change
Updates `docs/workers/iii-state.mdx` to surface the nested-path support that lands with this PR and the FR-9 Case-2 transition. - Append's example row in the operations table swapped to the nested-path form to make the new shape obvious at a glance. - The "first-level field names" sentence narrowed to set / increment / decrement / remove (the four ops still gated to single-string paths). Merge + append now share a paragraph + side-by-side code examples covering root / first-level / nested paths. - New explicit note on the FR-11 nested-vs-single-path divergence for missing leaves: nested missing leaves are ALWAYS arrays even for string values, while single-string paths preserve the legacy string-concat tier. This is the surface-level user-facing rule. - New **Behavior change worth noting** callout on the Case-2 transition (object/scalar leaf -> `append.type_mismatch` rather than silent no-op). Marks the boundary between back-compat payloads (unchanged) and the documented break. - Two new error code rows: `append.path.too_deep` and `append.path.empty_segment` mirror the merge-side validation surface. Refs #1552. Ridden with [Loa](https://github.com/0xHoneyJar/loa)
1 parent 066a638 commit 4ac386d

1 file changed

Lines changed: 16 additions & 7 deletions

File tree

docs/workers/iii-state.mdx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,27 +182,34 @@ name: bridge
182182
| `merge` | `{ "type": "merge", "path": ["sessions", "abc"], "value": { "ts": "chunk" } }` | Shallow-merge an object at the root or at any nested path. |
183183
| `increment` | `{ "type": "increment", "path": "count", "by": 1 }` | Add `by` to a numeric field. |
184184
| `decrement` | `{ "type": "decrement", "path": "count", "by": 1 }` | Subtract `by` from a numeric field. |
185-
| `append` | `{ "type": "append", "path": "events", "value": { "kind": "chunk" } }` | Push one element to an array or concatenate a string value. |
185+
| `append` | `{ "type": "append", "path": ["sessions", "abc", "events"], "value": { "kind": "chunk" } }` | Push one element to an array (or concatenate a string) at the root, a first-level field, or any nested path. |
186186
| `remove` | `{ "type": "remove", "path": "status" }` | Remove a field from the current object. |
187187

188-
For `set`, `increment`, `decrement`, `append`, and `remove`, paths are first-level field names. For example, `user.name` updates the field named `user.name`; it does not traverse into `{ "user": { "name": ... } }`.
188+
For `set`, `increment`, `decrement`, and `remove`, paths are first-level field names. For example, `user.name` updates the field named `user.name`; it does not traverse into `{ "user": { "name": ... } }`.
189189

190-
For `merge`, `path` accepts either a single string (legacy / first-level field) or an array of literal segments for nested merge:
190+
For `merge` and `append`, `path` accepts either a single string (legacy / first-level field) or an array of literal segments for nested traversal:
191191

192192
```json
193-
// Root merge (existing behavior, unchanged).
193+
// Root merge / append (existing behavior, unchanged).
194194
{ "type": "merge", "path": "", "value": { "status": "active" } }
195+
{ "type": "append", "path": "", "value": "first" }
195196
196-
// First-level merge into the field named "session-abc".
197+
// First-level merge / append into the field named "session-abc".
197198
{ "type": "merge", "path": "session-abc", "value": { "author": "alice" } }
199+
{ "type": "append", "path": "events", "value": { "kind": "chunk" } }
198200
199-
// Nested merge: walks "sessions" → "abc", auto-creating
200-
// missing or non-object intermediates as it goes.
201+
// Nested merge / append: walks the segments, auto-creating
202+
// missing or non-object intermediates along the way.
201203
{ "type": "merge", "path": ["sessions", "abc"], "value": { "ts": "chunk" } }
204+
{ "type": "append", "path": ["sessions", "abc", "events"], "value": { "kind": "chunk" } }
202205
```
203206

204207
Each array element is a *literal* key. `["a.b"]` writes a single key named `"a.b"`, not `a → b`.
205208

209+
**Append at a nested missing leaf is always an array.** When `append` walks to a missing leaf at the end of an array-form path, it creates `[value]` regardless of the value's type — including string values, which would be kept as a string under the legacy single-string path's string-concat tier. This is the core fix for [issue #1552](https://github.com/iii-hq/iii/issues/1552).
210+
211+
**Behavior change worth noting (issue #1552 case 2):** `append` at a path whose existing leaf is a JSON object or scalar (number/boolean) — i.e. a value that can't be appended to — now returns a structured `append.type_mismatch` error in the response `errors` array. Previously this would silently no-op when reached via the single-string path. Existing callers using `path: ""` or `path: "field"` against array, string, null, or missing-field leaves are unaffected.
212+
206213
Validation: invalid update inputs are rejected with a structured error in the response's `errors` array. Reasons include path depth > 32 segments, segment > 256 bytes, value depth > 16, > 1024 top-level keys, type mismatches, non-object targets, or any segment / top-level key matching `__proto__` / `constructor` / `prototype`. Successfully applied ops still reflect in `new_value`.
207214
</ResponseField>
208215
</Accordion>
@@ -238,6 +245,8 @@ Each `state::update` op may add an entry to the response `errors` array. Operati
238245
| `<op>.path.segment_too_long` | A path segment is longer than 256 bytes | Shorten the field name or merge path segment. |
239246
| `merge.path.too_deep` | A nested merge path has more than 32 segments | Reduce the nested path depth. |
240247
| `merge.path.empty_segment` | A nested merge path array contains an empty segment | Remove the empty segment. |
248+
| `append.path.too_deep` | A nested append path has more than 32 segments | Reduce the nested path depth. |
249+
| `append.path.empty_segment` | A nested append path array contains an empty segment | Remove the empty segment. |
241250
| `merge.value.not_an_object` | `merge` value is not a JSON object | Pass an object as the merge value. |
242251
| `merge.value.too_deep` | `merge` value has JSON nesting deeper than 16 levels | Flatten the value. |
243252
| `merge.value.too_many_keys` | `merge` value has more than 1024 top-level keys | Split the write into smaller updates. |

0 commit comments

Comments
 (0)