Skip to content

Commit 37b1346

Browse files
os-zhuangclaude
andauthored
feat(storage): surface sys_file id on upload-complete — ADR-0104 D3 wave 2 (PR-1) (#3466)
POST /api/v1/storage/upload/complete now returns the opaque sys_file id (data.fileId), and client.storage.upload() surfaces it on the returned FileMetadata. Previously the commit response omitted the id, so a caller could not learn which id to persist — a file field could never store a reference. Additive/non-breaking: new optional fileId on FileMetadataSchema; the client falls back to the presigned id against an older server. The enabling foundation for file-as-reference; the storage model is unchanged here. Tests: service-storage 99 green (complete-response fileId asserted); spec storage schemas green; client typecheck clean; generated storage reference doc regenerated. Claude-Session: https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd Co-authored-by: Claude <noreply@anthropic.com>
1 parent cde1975 commit 37b1346

6 files changed

Lines changed: 40 additions & 2 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/service-storage": patch
4+
"@objectstack/client": patch
5+
---
6+
7+
feat(storage): surface the sys_file id on upload-complete — ADR-0104 D3 wave 2 (PR-1)
8+
9+
`POST /api/v1/storage/upload/complete` now returns the opaque `sys_file` id
10+
(`data.fileId`), and `client.storage.upload()` surfaces it on the returned
11+
`FileMetadata`. Previously the commit response omitted the id — the caller
12+
could not learn which id to persist after committing an upload, so a file
13+
field could never store a reference.
14+
15+
Additive and non-breaking (new optional `fileId` on `FileMetadataSchema`; the
16+
client falls back to the presigned id when talking to an older server). This is
17+
the enabling foundation for file-as-reference; the storage model itself is
18+
unchanged in this PR.

content/docs/references/system/object-storage.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ const result = AccessControlConfig.parse(data);
9999
| **lastModified** | `string` || Last modified timestamp |
100100
| **created** | `string` || Creation timestamp |
101101
| **etag** | `string` | optional | Entity tag |
102+
| **fileId** | `string` | optional | Opaque sys_file id (ADR-0104 D3 file-as-reference) |
102103

103104

104105
---

packages/client/src/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2048,8 +2048,15 @@ export class ObjectStackClient {
20482048
method: 'POST',
20492049
body: JSON.stringify(completeReq)
20502050
});
2051-
2052-
return completeRes.json();
2051+
2052+
// Surface the opaque sys_file id so the caller can store it in a file
2053+
// field (ADR-0104 D3). The server now returns it; fall back to the
2054+
// presigned id for an older server that does not.
2055+
const completeJson = (await completeRes.json()) as FileUploadResponse;
2056+
if (completeJson?.data && completeJson.data.fileId == null) {
2057+
completeJson.data.fileId = presigned.fileId;
2058+
}
2059+
return completeJson;
20532060
},
20542061

20552062
getDownloadUrl: async (fileId: string): Promise<string> => {

packages/services/service-storage/src/storage-routes.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ describe('Storage REST Routes', () => {
140140
expect(completeRes._status).toBe(200);
141141
expect(completeRes._json.data.name).toBe('test.txt');
142142
expect(completeRes._json.data.mimeType).toBe('text/plain');
143+
// ADR-0104 D3 PR-1: the commit response surfaces the opaque sys_file id
144+
// so a caller can persist it in a file field.
145+
expect(completeRes._json.data.fileId).toBe(fileId);
143146
});
144147

145148
it('should 404 for unknown fileId', async () => {

packages/services/service-storage/src/storage-routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ export function registerStorageRoutes(
230230

231231
res.json({
232232
data: {
233+
// The opaque sys_file id — the value a file field stores as a
234+
// reference (ADR-0104 D3). Previously omitted, so a caller could not
235+
// learn the id to persist after committing an upload.
236+
fileId: updated!.id ?? fileId,
233237
path: updated!.key,
234238
name: updated!.name,
235239
size: updated!.size ?? 0,

packages/spec/src/system/object-storage.zod.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ export const FileMetadataSchema = lazySchema(() => z.object({
5353
lastModified: z.string().datetime().describe('Last modified timestamp'),
5454
created: z.string().datetime().describe('Creation timestamp'),
5555
etag: z.string().optional().describe('Entity tag'),
56+
// The opaque `sys_file` id (ADR-0104 D3): the value a file/image/avatar/
57+
// video/audio field will store as a reference. Surfaced on the upload-complete
58+
// response so a caller can persist it; optional because generic file-metadata
59+
// reads (getInfo/list) do not always carry it.
60+
fileId: z.string().optional().describe('Opaque sys_file id (ADR-0104 D3 file-as-reference)'),
5661
}));
5762

5863
export type FileMetadata = z.infer<typeof FileMetadataSchema>;

0 commit comments

Comments
 (0)