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
fix(service-storage): give failed/expired upload-session statuses a producer (#7667) (#7844)
* fix(service-storage): give `failed`/`expired` upload-session statuses a producer (#7667)
`sys_upload_session.status` declared `failed` and `expired`, the retention
backstop reaped on both, and `UploadProgressSchema` published both to every
client reading the contract — while nothing in the service ever wrote either.
A scan of every session row could only return `in_progress`/`completed`, so
the retention rule named two states the system could not enter.
ADR-0049 enforce-or-remove, taking the ENFORCE branch: removal would have
forked the object from the spec's progress contract, and both failure states
are real and were previously invisible.
- `failed`: a completion whose backend `completeChunkedUpload` threw left the
row at `completing` — non-terminal, so the 7d retention backstop never
reaped it and a progress poll read "still assembling" indefinitely. The
completion route now stamps `failed` on that path. It records an attempt
rather than locking the session: a retry runs the happy path and overwrites
it with `completed`.
- `expired`: a session past its own `expires_at` kept answering `in_progress`
and kept accepting chunks until the TTL sweep deleted the row out from under
the caller, so the deadline the init response announced bound nothing. A
chunk PUT or a complete against an overdue session is now refused 410
`UPLOAD_SESSION_EXPIRED` (registered under `@objectstack/service-storage` in
`ERROR_CODE_LEDGER`) and the row is durably stamped `expired`. Progress
REPORTS the status rather than refusing — `expired` is a declared member of
`UploadProgressSchema.status` and the SDK's `resumeUpload` polls it first.
A row with no `expires_at` carries no declared deadline and is left alone; a
`completed` row does not become `expired` by waiting for the reaper. The
`failed` stamp is best-effort and loud on failure, so a metadata-store error
never replaces the real backend cause on its way to the 500.
Checklist item `attachments-storage.upload-session-abort` revision 3 records
the producers, adds steps that drive both statuses, and records transient
`completing` as a knownGap rather than an unreachable-variant FAIL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198Jr94CUGy2vDGtT1L8pka
* docs(spec): regenerate API reference for the UPLOAD_SESSION_EXPIRED ledger entry (#7667)
产物随源走: registering `UPLOAD_SESSION_EXPIRED` in `ERROR_CODE_LEDGER` widens
the `ErrorCode` union every enveloped response references, so all 11
`content/docs/references/api/*.mdx` pages that render it were stale and
`check:docs` (`build-docs.ts --check`) failed the TypeScript Type Check job.
The whole diff is that one addition propagating: a new `UPLOAD_SESSION_EXPIRED`
bullet in `error-code-ledger.mdx`, and the union arity in every rendered `error`
column moving `+260 more` → `+261 more`. No unrelated drift was absorbed.
Generated, not hand-written: `pnpm --filter @objectstack/spec gen:schema && gen:docs`
on a clean tree with no merge in progress (#5370). `authorable-surface.base.json`
was not touched, so no re-anchoring rode along; `json-schema/openapi.json` was not
cleared by the rmSync and was refreshed anyway (#5371, gitignored either way).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198Jr94CUGy2vDGtT1L8pka
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|**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. |
50
50
@@ -79,7 +79,7 @@ const result = AnalyticsEndpoint.parse(data);
79
79
| Property | Type | Required | Description |
80
80
| :--- | :--- | :--- | :--- |
81
81
|**success**|`boolean`| ✅ | Operation success status |
Copy file name to clipboardExpand all lines: content/docs/references/api/batch.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
@@ -55,7 +55,7 @@ const result = BatchConfigSchema.parse(data);
55
55
| :--- | :--- | :--- | :--- |
56
56
|**id**|`string`| optional | Record ID if operation succeeded |
57
57
|**success**|`boolean`| ✅ | Whether this record was processed successfully |
58
-
|**errors**|`{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 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). |
58
+
|**errors**|`{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +261 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). |
59
59
|**data**|`Record<string, any>`| optional | Full record data (if returnRecords=true) |
60
60
|**index**|`number`| optional | Index of the record in the request array |
61
61
|**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. |
@@ -122,7 +122,7 @@ const result = BatchConfigSchema.parse(data);
122
122
| Property | Type | Required | Description |
123
123
| :--- | :--- | :--- | :--- |
124
124
|**success**|`boolean`| ✅ | Operation success status |
0 commit comments