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
41 changes: 41 additions & 0 deletions .changeset/actions-door-demotes-author-codes.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
---
"@objectstack/spec": minor
"@objectstack/types": minor
"@objectstack/runtime": minor
---

`error.code` is a closed vocabulary at every door (#9106, maintainer ruling
2026-08-16): the runtime dispatcher's thrown-error exits
(`HttpDispatcher.errorFromThrown`, `dispatcher-plugin`'s `errorResponseBase`,
`endpoint-executor`'s `endpointErrorAnswer` — the actions door among them) now
serve the narrowed `code` the shared resolver (`resolveThrownHttpError`,
`@objectstack/types`) has always computed, exactly as the REST door has since
#8016. A thrown code that is not a member of `StandardErrorCode ∪
ERROR_CODE_LEDGER` no longer reaches `error.code`.

It is not dropped: `ApiErrorSchema` declares a new optional `declaredCode`
field — the open, author-authored channel — and the demoted spelling rides
there. Presence means demotion: the field is absent whenever the producer's
code is a vocabulary member (it is already in `error.code`) or the producer
declared none. The #7867 sandbox passthrough capability is preserved — a
metadata app's own thrown `.code` still crosses the QuickJS boundary and still
reaches the wire.

For a metadata app that throws its own code (e.g.
`Object.assign(new Error('pick another'), { code: 'DUPLICATE' })` in an action
body) and reads it back from an actions-door failure:

- FROM: `error.code === 'DUPLICATE'`
- TO: `error.code` is the closed member the status derives (e.g.
`VALIDATION_ERROR` on a 400) and `error.declaredCode === 'DUPLICATE'`.
One-line fix: branch on `error.declaredCode` for app-specific spellings;
branch on `error.code` for platform conditions.

Platform producers are unaffected: every registered code reaches `error.code`
verbatim, as before (post-#8846 the dispatcher-vocabulary gate holds that set
registered). Measured before landing (the ruling's binding precondition): no
existing consumer of the actions door branches on author-authored strings in
`error.code`.

`@objectstack/types` adds `demotedDeclaredCode(thrown)` — the one definition of
"which spelling a boundary surfaces beside the closed `code`".
6 changes: 3 additions & 3 deletions content/docs/references/api/analytics.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,7 @@ const result = AnalyticsEndpoint.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ name: string; title?: string; measures: object[]; dimensions: object[] }[]` | ✅ | Available cubes, each as the `CubeMeta` discovery projection — the cube name, its title, and the measures/dimensions a client may name in a query. A bare array: there is no `cubes` wrapper object, and no cube `sql` is published. |

Expand DownExpand Up@@ -79,7 +79,7 @@ const result = AnalyticsEndpoint.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ rows: Record<string, any>[]; fields: object[]; sql?: string }` | ✅ | |

Expand All@@ -93,7 +93,7 @@ const result = AnalyticsEndpoint.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ sql: string; params: any[] }` | ✅ | |

Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/api/auth.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,7 +117,7 @@ const result = AuthProvider.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ session: object; user: object; token?: string }` | ✅ | |

Expand DownExpand Up@@ -153,7 +153,7 @@ const result = AuthProvider.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ id: string; email: string; emailVerified: boolean; name: string; … }` | ✅ | |

Expand Down
18 changes: 9 additions & 9 deletions content/docs/references/api/automation-api.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,7 +119,7 @@ const result = AutomationApiErrorCode.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | The created flow definition |

Expand All@@ -144,7 +144,7 @@ const result = AutomationApiErrorCode.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ name: string; deleted: boolean }` | ✅ | |

Expand DownExpand Up@@ -187,7 +187,7 @@ const result = AutomationApiErrorCode.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | Full flow definition |

Expand All@@ -213,7 +213,7 @@ const result = AutomationApiErrorCode.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ id: string; flowName: string; flowVersion?: integer; status: Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| … +2 more>; … }` | ✅ | Full execution log with step details |

Expand DownExpand Up@@ -241,7 +241,7 @@ const result = AutomationApiErrorCode.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ flows: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |

Expand DownExpand Up@@ -269,7 +269,7 @@ const result = AutomationApiErrorCode.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ runs: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |

Expand All@@ -295,7 +295,7 @@ const result = AutomationApiErrorCode.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ name: string; enabled: boolean }` | ✅ | |

Expand DownExpand Up@@ -325,7 +325,7 @@ const result = AutomationApiErrorCode.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ success: boolean; output?: any; error?: string; durationMs?: number }` | ✅ | |

Expand All@@ -351,7 +351,7 @@ const result = AutomationApiErrorCode.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | The updated flow definition |

Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/api/batch.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,7 @@ const result = BatchConfigSchema.parse(data);
| :--- | :--- | :--- | :--- |
| **id** | `string` | optional | Record ID if operation succeeded |
| **success** | `boolean` | ✅ | Whether this record was processed successfully |
| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). |
| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). |
| **data** | `Record<string, any>` | optional | Full record data (if returnRecords=true) |
| **index** | `number` | optional | Index of the record in the request array |
| **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static `readonly` (#2948) / TRUE `readonlyWhen` (#3042) on update, or the #3043 create-ingress strip. Per-row because a batch can drop different fields on different rows (`readonlyWhen` is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit `X-ObjectStack-Dropped-Fields` for batches. Optional — omit-when-empty keeps the shape backward-compatible. |
Expand DownExpand Up@@ -122,7 +122,7 @@ const result = BatchConfigSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +285 more>; declaredCode?: string; message: string; category?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
| **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` | optional | Operation type that was performed |
| **total** | `number` | ✅ | Total number of records in the batch |
Expand Down
Loading
Loading