Skip to content

Commit e4977aa

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22642-flow-cel-record-binding
2 parents 59a414c + 6a3fe25 commit e4977aa

16 files changed

Lines changed: 520 additions & 57 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-storage': patch
4+
'@objectstack/client': minor
5+
---
6+
7+
feat(spec,service-storage,client)!: one upload-scope list — the upload requests' `scope` and the SDK's `storage.upload` close to the new `UploadScope` enum, the `sys_file` scope select is built from it, and the upload doors answer any other scope with `400` naming the allowed values instead of `500 INTERNAL` (#22470)
8+
9+
Clause-②: yes (narrowing)
10+
11+
<!-- adr-0087: registered upload-request-scope-closed -->
12+
13+
**BREAKING** — an accept-set narrowing on a published request contract and on the SDK method that sends it, shipped as `minor` under the launch-window convention for accept-set narrowings, plus one new export.
14+
15+
The presigned and chunked upload requests declared `scope` an open string, while the stored file record (`sys_file.scope`, a closed select) only ever took `user`, `tenant`, `private`, `temp` and `attachments`. Any other value reached the `sys_file` insert, the data engine refused it as an invalid option, and the upload door answered that caller error as `500 INTERNAL`, with a message telling the operator to restore the data engine.
16+
17+
### What changes
18+
19+
- **`UploadScopeSchema` / `UploadScope`** (`@objectstack/spec`, new, exported from `@objectstack/spec/api`): the upload-scope vocabulary, declared once — `user`, `tenant`, `private`, `temp`, `attachments`. It is a different list from `StorageScopeSchema`, which classifies a storage configuration and is not read by any upload.
20+
- **`GetPresignedUrlRequestSchema.scope` and `InitiateChunkedUploadRequestSchema.scope`** read it. The default stays `user`. A literal outside the list fails `tsc`, and a parse refuses it on the `scope` key.
21+
- **`client.storage.upload(file, scope)`** (`@objectstack/client`): the `scope` parameter is typed `UploadScope` instead of `string`, default `user` unchanged, so a scope outside the list fails `tsc` at the SDK call. `client.storage.getPresignedUrl` and `client.storage.initChunkedUpload` take the request types above and narrow with them.
22+
- **The `sys_file` scope select** (`@objectstack/service-storage`) takes its options from the enum, in its order, with the same labels. The stored values do not change.
23+
- **The presigned upload door and the chunked upload door** answer a scope outside the list — any string, a case variant, `null`, a number — with `400 INVALID_REQUEST`, naming the allowed values, before a file record, a session record, an upload URL or a backend upload exists. `public` is refused by the same gate and keeps its remedy (`acl: 'public_read'` on the stored file record). An omitted scope is the default `user`, as before. A real data-engine fault still answers `500`.
24+
25+
### FROM → TO
26+
27+
| before | what to write instead |
28+
| --- | --- |
29+
| an upload naming a scope outside the list (a key prefix such as `avatars`, a folder, a record path) | one of `user`, `tenant`, `private`, `temp`, `attachments`, or no `scope` for the default `user` |
30+
| `client.storage.upload(file, scope)` called with a `scope` typed `string` (`@objectstack/client`) | pass one of the five, or type the value `UploadScope` (`import type { UploadScope } from '@objectstack/spec/api'`) |
31+
| a caller passing a scope typed `string` into either upload request | type it `UploadScope` |
32+
33+
**The one-line fix: send one of the five upload scopes, or none.** `attachments` is for a file whose referrers are record attachment rows (it is what orphan tombstoning reads); `temp` for a scratch file; otherwise `user` or `tenant`.
34+
35+
**No stored file record moves**: no upload naming another scope ever succeeded, so no stored record carries one.
36+
37+
### The kit
38+
39+
- **D3 entry `upload-request-scope-closed`** carries the judgement no rewrite can make: which scope a file that was sent under a free name really belongs under. No D2 conversion: no metadata type carries the upload request, so `os migrate meta` has no authored source to rewrite.
40+
- **Liveness.** No ledger row: the liveness ledger walks metadata types, and no metadata type carries the upload request.

‎content/docs/references/api/storage.mdx‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ rather than proxying bytes through the API server.
1919
## TypeScript Usage
2020

2121
```typescript
22-
import { CompleteChunkedUploadRequestSchema, CompleteChunkedUploadResponseSchema, CompleteUploadRequestSchema, FileDownloadUrlResponseSchema, FileTypeValidationSchema, FileUploadResponseSchema, GetPresignedUrlRequestSchema, InitiateChunkedUploadRequestSchema, InitiateChunkedUploadResponseSchema, PresignedUrlResponseSchema, RawUploadResponseSchema, UploadChunkRequestSchema, UploadChunkResponseSchema, UploadProgressSchema } from '@objectstack/spec/api';
23-
import type { CompleteChunkedUploadRequest, CompleteChunkedUploadResponse, CompleteUploadRequest, FileDownloadUrlResponse, FileTypeValidation, FileUploadResponse, GetPresignedUrlRequest, InitiateChunkedUploadRequest, InitiateChunkedUploadResponse, PresignedUrlResponse, RawUploadResponse, UploadChunkRequest, UploadChunkResponse, UploadProgress } from '@objectstack/spec/api';
22+
import { CompleteChunkedUploadRequestSchema, CompleteChunkedUploadResponseSchema, CompleteUploadRequestSchema, FileDownloadUrlResponseSchema, FileTypeValidationSchema, FileUploadResponseSchema, GetPresignedUrlRequestSchema, InitiateChunkedUploadRequestSchema, InitiateChunkedUploadResponseSchema, PresignedUrlResponseSchema, RawUploadResponseSchema, UploadChunkRequestSchema, UploadChunkResponseSchema, UploadProgressSchema, UploadScopeSchema } from '@objectstack/spec/api';
23+
import type { CompleteChunkedUploadRequest, CompleteChunkedUploadResponse, CompleteUploadRequest, FileDownloadUrlResponse, FileTypeValidation, FileUploadResponse, GetPresignedUrlRequest, InitiateChunkedUploadRequest, InitiateChunkedUploadResponse, PresignedUrlResponse, RawUploadResponse, UploadChunkRequest, UploadChunkResponse, UploadProgress, UploadScope } from '@objectstack/spec/api';
2424

2525
// Validate data
2626
const result = CompleteChunkedUploadRequestSchema.parse(data);
@@ -224,7 +224,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data);
224224
| **filename** | `string` | ✅ | Original filename |
225225
| **mimeType** | `string` | ✅ | File MIME type |
226226
| **size** | `number` | ✅ | File size in bytes |
227-
| **scope** | `string` | optional (default: `"user"`) | Storage scope the new file is filed under (default user; attachments for the record attachments surface). Not an access setting: the upload doors refuse public, and a file is served without sign-in only when its stored file record carries acl 'public_read' (ADR-0104). The upload request carries no acl; every upload is stored private. |
227+
| **scope** | `Enum<'user' \| 'tenant' \| 'private' \| 'temp' \| 'attachments'>` | optional (default: `"user"`) | Storage scope the new file is filed under (default user; attachments for the record attachments surface). Not an access setting: the upload doors refuse public, and a file is served without sign-in only when its stored file record carries acl 'public_read' (ADR-0104). The upload request carries no acl; every upload is stored private. |
228228
| **bucket** | `string` | optional | Specific bucket override (admin only) |
229229

230230

@@ -240,7 +240,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data);
240240
| **mimeType** | `string` | ✅ | File MIME type |
241241
| **totalSize** | `integer` | ✅ | Total file size in bytes |
242242
| **chunkSize** | `integer` | optional (default: `5242880`) | Size of each chunk in bytes (minimum 5MB per S3 spec) |
243-
| **scope** | `string` | optional (default: `"user"`) | Storage scope the new file is filed under (default user; attachments for the record attachments surface). Not an access setting: the upload doors refuse public, and a file is served without sign-in only when its stored file record carries acl 'public_read' (ADR-0104). The upload request carries no acl; every upload is stored private. |
243+
| **scope** | `Enum<'user' \| 'tenant' \| 'private' \| 'temp' \| 'attachments'>` | optional (default: `"user"`) | Storage scope the new file is filed under (default user; attachments for the record attachments surface). Not an access setting: the upload doors refuse public, and a file is served without sign-in only when its stored file record carries acl 'public_read' (ADR-0104). The upload request carries no acl; every upload is stored private. |
244244
| **bucket** | `string` | optional | Specific bucket override (admin only) |
245245
| **metadata** | `Record<string, string>` | optional | Custom metadata key-value pairs |
246246

@@ -497,3 +497,18 @@ const result = CompleteChunkedUploadRequestSchema.parse(data);
497497

498498
---
499499

500+
## UploadScope
501+
502+
Storage scope an uploaded file is filed under
503+
504+
### Allowed Values
505+
506+
* `user`
507+
* `tenant`
508+
* `private`
509+
* `temp`
510+
* `attachments`
511+
512+
513+
---
514+

‎content/docs/references/index.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Protocol reference — every schema by module
33
navTitle: Protocol Reference
4-
description: Every schema published by @objectstack/spec — 1515 schemas across 14 protocol modules
4+
description: Every schema published by @objectstack/spec — 1516 schemas across 14 protocol modules
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -21,7 +21,7 @@ counts are sums of the rows they head. Regenerate with
2121
| Module | Pages | Schemas | Description |
2222
| :--- | ---: | ---: | :--- |
2323
| [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. |
24-
| [API Protocol](/docs/references/api) | 32 | 431 | REST contracts, endpoints, routing, realtime, batch, discovery. |
24+
| [API Protocol](/docs/references/api) | 32 | 432 | REST contracts, endpoints, routing, realtime, batch, discovery. |
2525
| [Automation Protocol](/docs/references/automation) | 13 | 70 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
2626
| [Data Protocol](/docs/references/data) | 29 | 175 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
2727
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
@@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with
3434
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3535
| [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3636
| [UI Protocol](/docs/references/ui) | 16 | 166 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
37-
| **Total** | **195** | **1515** | 14 protocol modules |
37+
| **Total** | **195** | **1516** | 14 protocol modules |
3838

3939
---
4040

@@ -63,7 +63,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations.
6363

6464
## API Protocol
6565

66-
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **32 pages, 431 schemas**
66+
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **32 pages, 432 schemas**
6767

6868
REST contracts, endpoints, routing, realtime, batch, discovery.
6969

@@ -98,7 +98,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery.
9898
| [`rest-server.zod.ts`](/docs/references/api/rest-server) | `BatchEndpointsConfig`, `CrudEndpointsConfig`, `CrudOperation`, `EndpointRegistry`, `GeneratedEndpoint`, `MetadataEndpointsConfig`, `RestApiConfig`, `RestServerConfig`, `RouteGenerationConfig` |
9999
| [`router.zod.ts`](/docs/references/api/router) | `ConflictResolutionStrategy`, `HttpMethod`, `RouteCategory`, `RouteDefinition`, `RouterConfig` |
100100
| [`sortability.zod.ts`](/docs/references/api/sortability) | `FieldSortability`, `ObjectSortability` |
101-
| [`storage.zod.ts`](/docs/references/api/storage) | `CompleteChunkedUploadRequest`, `CompleteChunkedUploadResponse`, `CompleteUploadRequest`, `FileDownloadUrlResponse`, `FileTypeValidation`, `FileUploadResponse`, `GetPresignedUrlRequest`, `InitiateChunkedUploadRequest`, `InitiateChunkedUploadResponse`, `PresignedUrlResponse`, `RawUploadResponse`, `UploadChunkRequest`, `UploadChunkResponse`, `UploadProgress` |
101+
| [`storage.zod.ts`](/docs/references/api/storage) | `CompleteChunkedUploadRequest`, `CompleteChunkedUploadResponse`, `CompleteUploadRequest`, `FileDownloadUrlResponse`, `FileTypeValidation`, `FileUploadResponse`, `GetPresignedUrlRequest`, `InitiateChunkedUploadRequest`, `InitiateChunkedUploadResponse`, `PresignedUrlResponse`, `RawUploadResponse`, `UploadChunkRequest`, `UploadChunkResponse`, `UploadProgress`, `UploadScope` |
102102
| [`versioning.zod.ts`](/docs/references/api/versioning) | `VersionDefinition`, `VersionNegotiationResponse`, `VersionStatus`, `VersioningConfig`, `VersioningStrategy` |
103103
| [`websocket.zod.ts`](/docs/references/api/websocket) | `AckMessage`, `CursorMessage`, `CursorPosition`, `DocumentState`, `EditMessage`, `EditOperation`, `EditOperationType`, `ErrorMessage`, `EventMessage`, `EventPattern`, `EventSubscription`, `PingMessage`, `PongMessage`, `PresenceMessage`, `PresenceState`, `PresenceUpdate`, `SimpleCursorPosition`, `SimplePresenceState`, `SubscribeMessage`, `UnsubscribeMessage`, `UnsubscribeRequest`, `WebSocketConfig`, `WebSocketEvent`, `WebSocketMessage`, `WebSocketMessageType`, `WebSocketPresenceStatus`, `WebSocketServerConfig` |
104104

‎packages/client/src/index.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
CompleteChunkedUploadRequest,
3838
CompleteChunkedUploadResponse,
3939
UploadProgress,
40+
UploadScope,
4041
ListNotificationsResponse,
4142
MarkNotificationsReadResponse,
4243
MarkAllNotificationsReadResponse,
@@ -5189,7 +5190,7 @@ export class ObjectStackClient {
51895190
* Storage Services
51905191
*/
51915192
storage = {
5192-
upload: async (file: any, scope: string = 'user'): Promise<FileUploadResponse> => {
5193+
upload: async (file: any, scope: UploadScope = 'user'): Promise<FileUploadResponse> => {
51935194
// 1. Get Presigned URL
51945195
const presignedReq: GetPresignedUrlRequest = {
51955196
filename: file.name,

‎packages/services/service-storage/src/objects/system-file.object.ts‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { ObjectSchema, Field } from '@objectstack/spec/data';
4+
import { UploadScopeSchema, type UploadScope } from '@objectstack/spec/api';
5+
6+
/**
7+
* Display labels of the `scope` select, one per upload scope.
8+
*
9+
* The VALUES are not listed here: the select reads them off
10+
* `UploadScopeSchema`, the one upload-scope list `@objectstack/spec` declares
11+
* for the upload requests and the upload doors too (#22470), so the store, the
12+
* request and the doors cannot drift apart. `Record<UploadScope, …>` makes a
13+
* member added to that enum without a label here a compile error.
14+
*/
15+
const UPLOAD_SCOPE_LABELS: Record<UploadScope, string> = {
16+
user: 'User',
17+
tenant: 'Tenant',
18+
private: 'Private',
19+
temp: 'Temp',
20+
// Files uploaded through the generic Attachments surface (#2727).
21+
// Their only legitimate referrers are sys_attachment join rows, so
22+
// this scope is the discriminator for orphan tombstoning (#2755) —
23+
// field-attachment scopes above are never tombstoned.
24+
attachments: 'Attachments',
25+
};
426

527
/**
628
* System File Object
@@ -62,19 +84,12 @@ export const SystemFile = ObjectSchema.create({
6284
// are rewritten to `user` by `backfill-sys-file-public-scope.ts`, the
6385
// operator step that must run before a copy of such a row can succeed.
6486
// Anonymous download is `acl: 'public_read'` and nothing else.
87+
//
88+
// The options are `UploadScopeSchema`'s values, in its order, each with
89+
// its label above — the list the upload requests and doors read (#22470).
6590
scope: Field.select({
6691
label: 'Scope',
67-
options: [
68-
{ label: 'User', value: 'user' },
69-
{ label: 'Tenant', value: 'tenant' },
70-
{ label: 'Private', value: 'private' },
71-
{ label: 'Temp', value: 'temp' },
72-
// Files uploaded through the generic Attachments surface (#2727).
73-
// Their only legitimate referrers are sys_attachment join rows, so
74-
// this scope is the discriminator for orphan tombstoning (#2755) —
75-
// field-attachment scopes above are never tombstoned.
76-
{ label: 'Attachments', value: 'attachments' },
77-
],
92+
options: UploadScopeSchema.options.map((value) => ({ label: UPLOAD_SCOPE_LABELS[value], value })),
7893
}),
7994

8095
bucket: Field.text({

0 commit comments

Comments
 (0)