Skip to content

Commit d2a2cde

Browse files
committed
docs: the attachments panel attaches in the call that completes its upload
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc
1 parent f646b60 commit d2a2cde

3 files changed

Lines changed: 43 additions & 2 deletions

File tree

‎content/docs/api/plugin-endpoints.mdx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ other adapter.
154154
| Method | Endpoint | SDK | Description |
155155
|:-------|:---------|:----|:------------|
156156
| POST | `/storage/upload/presigned` | `storage.getPresignedUrl` | Step 1 — mint an upload target for a new `sys_file` row |
157-
| POST | `/storage/upload/complete` | `storage.upload` | Step 3 — commit the uploaded bytes |
157+
| POST | `/storage/upload/complete` | `storage.upload` | Step 3 — commit the uploaded bytes; with `attachTo`, also attach the file to a record as the caller |
158158
| POST | `/storage/upload/chunked` | `storage.initChunkedUpload` | Open a chunked / resumable session |
159159
| PUT | `/storage/upload/chunked/:uploadId/chunk/:chunkIndex` | `storage.uploadPart` | Send one chunk |
160160
| POST | `/storage/upload/chunked/:uploadId/complete` | `storage.completeChunkedUpload` | Assemble the parts |
@@ -167,6 +167,13 @@ other adapter.
167167
so for a file whose type the browser does not know (`file.type === ''`) the
168168
helper sends `application/octet-stream`; a typed file keeps its own type.
169169

170+
To attach the upload to a record's Attachments, send `attachTo: { parentObject,
171+
parentId }` with step 3 (and start the upload with `scope: 'attachments'`): the
172+
commit and the `sys_attachment` row are one unit, written as the signed-in
173+
caller, and the answer carries the `attachmentId`
174+
([attachment access](/docs/permissions/attachments-access)). A refused attach
175+
leaves the upload pending.
176+
170177
A file field takes the `fileId` only after step 3. A signed-in record write
171178
that names a file whose upload was never completed is refused with `400
172179
VALIDATION_FAILED`, one `invalid_value` entry naming the field and the file,

‎content/docs/permissions/attachments-access.mdx‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,39 @@ drops a non-system caller's `uploaded_by` and reports it in `droppedFields`,
7272
and the rest of the write lands. System-context writes keep the value they
7373
supply.
7474

75+
### Attaching an upload from the browser
76+
77+
The Attachments panel attaches a file in the call that completes its upload:
78+
the presigned upload's `POST /api/v1/storage/upload/complete` carries
79+
`attachTo: { parentObject, parentId }`, and the server commits the file and
80+
writes its `sys_attachment` row as one unit, **as the signed-in user**. Every
81+
check above judges that user: the permission to create an attachment, edit
82+
access to the record, the record object's `enable.files`, and the file
83+
reference itself. The answer carries the new `attachmentId`. A refused attach
84+
leaves the upload `pending` and attached to nothing, and the pending-upload
85+
retention reclaims it.
86+
87+
The file must be the caller's own upload, still `pending`, in the
88+
`attachments` scope, attached to nothing and owned by no record field, with its
89+
bytes stored. Any other file id — another user's upload, an id that names no
90+
file — is refused before anything is written:
91+
92+
| Code | Status | When |
93+
| --- | --- | --- |
94+
| `PERMISSION_DENIED` | 403 | The file is another user's upload (the commit door's uploader rule) |
95+
| `FILE_NOT_FOUND` | 404 | The id names no file |
96+
| `RESOURCE_CONFLICT` | 409 | The upload is already completed, already attached or owned by a field, or its bytes are not stored |
97+
| `INVALID_REQUEST` | 400 | `attachTo` is malformed, or the upload was not started in the `attachments` scope |
98+
| `NOT_IMPLEMENTED` | 501 | The deployment composes no auth service or no data engine, so there is no caller to attach as |
99+
100+
This is how a member who may attach files but holds no read on `sys_file`
101+
attaches one: a `sys_attachment` create through the generic data API
102+
(`POST /api/v1/data/sys_attachment`) names its `file_id` as any other
103+
`lookup` value, which the caller must be able to read, and a file they cannot
104+
read is refused `400 VALIDATION_FAILED` (`reference_not_found` on `file_id`).
105+
The attach on completion vouches for that one file, in that one call, and
106+
grants no read of `sys_file` or of anything else.
107+
75108
| Code | Status | When |
76109
| --- | --- | --- |
77110
| `RECORD_NOT_ACCESSIBLE` | 403 | The session names no signed-in user to record as the uploader |
@@ -224,6 +257,7 @@ can be shared across records). Reclamation is handled by the platform LifecycleS
224257
| Operation | Requirement | Deny code |
225258
| --- | --- | --- |
226259
| Attach (create) | parent object opts in (`enable.files`) | `FILES_DISABLED` (403) |
260+
| Attach on upload completion (`attachTo`) | the caller's own pending, unattached `attachments`-scope upload, then every create check | `PERMISSION_DENIED` (403) / `RESOURCE_CONFLICT` (409), then the create codes |
227261
| Attach (create), re-point | a by-id update of the parent record by the caller would be admitted | `ATTACHMENT_PARENT_ACCESS` (403); `OBJECT_NOT_FOUND` (404) when the parent object is not registered |
228262
| List / read | inherits parent read visibility | *(filtered out)* |
229263
| Delete | uploader or parent editor (+ RBAC delete grant) | `ATTACHMENT_DELETE_DENIED` (403); `PERMISSION_DENIED` (403) when the parent is not readable or no delete grant is held |

‎packages/services/service-storage/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ All routes are mounted at `/api/v1/storage` (configurable via `basePath`).
129129
| Method | Path | Description |
130130
|--------|------|-------------|
131131
| POST | `/upload/presigned` | Get presigned upload URL |
132-
| POST | `/upload/complete` | Mark upload as committed |
132+
| POST | `/upload/complete` | Mark upload as committed; with `attachTo: { parentObject, parentId }`, attach the caller's own pending upload to that record as the caller, in the same unit |
133133
| POST | `/upload/chunked` | Initiate chunked upload |
134134
| PUT | `/upload/chunked/:uploadId/chunk/:chunkIndex` | Upload a chunk |
135135
| POST | `/upload/chunked/:uploadId/complete` | Complete chunked upload |

0 commit comments

Comments
 (0)