Skip to content

Commit 0dcbc11

Browse files
huangyiireneclaude
andauthored
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>
1 parent 66360f3 commit 0dcbc11

18 files changed

Lines changed: 431 additions & 82 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/service-storage': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
storage: `sys_upload_session.status` `failed` / `expired` now have producers
7+
8+
Both statuses were declared on the object, reaped on by the retention backstop
9+
(`onlyWhen: { status: { $in: ['completed', 'failed', 'expired'] } }`), and
10+
published to clients by `UploadProgressSchema` — while nothing in the service
11+
ever wrote either one. A scan of every session row could only return
12+
`in_progress` / `completed`, so retention named two states the system could not
13+
enter. Under ADR-0049 (enforce-or-remove) this takes the enforce branch:
14+
removing them would have forked the object from the spec's progress contract,
15+
and the two failure states they name are real.
16+
17+
- **`failed`** — a chunked completion whose backend `completeChunkedUpload`
18+
threw left the row at `completing`: a non-terminal status the 7d retention
19+
backstop never reaped, and one a progress poll reported as "still assembling"
20+
forever. The completion route now stamps `failed` on that path. It records an
21+
attempt rather than locking the session — a retry of the same `uploadId` runs
22+
the happy path and overwrites it with `completed`.
23+
- **`expired`** — a session past its own `expires_at` kept answering
24+
`in_progress` and kept accepting chunks until the TTL sweep deleted the row
25+
out from under the caller, so the deadline the init response already announced
26+
(`expiresAt`) bound nothing. A chunk `PUT` or a `complete` against an
27+
overdue session is now refused with **410 `UPLOAD_SESSION_EXPIRED`** (new code,
28+
registered under `@objectstack/service-storage` in `ERROR_CODE_LEDGER`) and the
29+
row is durably stamped `expired`. `GET .../progress` reports the status instead
30+
of refusing — `expired` is a declared member of `UploadProgressSchema.status`,
31+
and the SDK's `resumeUpload` reads progress first.
32+
33+
A session with no `expires_at` carries no declared deadline and is left alone,
34+
and a `completed` row does not become `expired` by waiting for the reaper.

content/docs/references/api/analytics.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const result = AnalyticsEndpoint.parse(data);
4444
| Property | Type | Required | Description |
4545
| :--- | :--- | :--- | :--- |
4646
| **success** | `boolean` || Operation success status |
47-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
47+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +261 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
4848
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
4949
| **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. |
5050

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

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

content/docs/references/api/auth.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ const result = AuthProvider.parse(data);
117117
| Property | Type | Required | Description |
118118
| :--- | :--- | :--- | :--- |
119119
| **success** | `boolean` || Operation success status |
120-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
120+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +261 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
121121
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
122122
| **data** | `{ session: object; user: object; token?: string }` || |
123123

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

content/docs/references/api/automation-api.mdx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const result = AutomationApiErrorCode.parse(data);
119119
| Property | Type | Required | Description |
120120
| :--- | :--- | :--- | :--- |
121121
| **success** | `boolean` || Operation success status |
122-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
122+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +261 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
123123
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
124124
| **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` || The created flow definition |
125125

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

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

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

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

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

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

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

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

content/docs/references/api/batch.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const result = BatchConfigSchema.parse(data);
5555
| :--- | :--- | :--- | :--- |
5656
| **id** | `string` | optional | Record ID if operation succeeded |
5757
| **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). |
5959
| **data** | `Record<string, any>` | optional | Full record data (if returnRecords=true) |
6060
| **index** | `number` | optional | Index of the record in the request array |
6161
| **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);
122122
| Property | Type | Required | Description |
123123
| :--- | :--- | :--- | :--- |
124124
| **success** | `boolean` || Operation success status |
125-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +260 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
125+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +261 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
126126
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
127127
| **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` | optional | Operation type that was performed |
128128
| **total** | `number` || Total number of records in the batch |

0 commit comments

Comments
 (0)