Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/adr-0104-d3w2-pr1-upload-complete-fileid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@objectstack/spec": patch
"@objectstack/service-storage": patch
"@objectstack/client": patch
---

feat(storage): surface the sys_file id on upload-complete — ADR-0104 D3 wave 2 (PR-1)

`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 — the caller
could not learn which id to persist after committing an upload, so a file
field could never store a reference.

Additive and non-breaking (new optional `fileId` on `FileMetadataSchema`; the
client falls back to the presigned id when talking to an older server). This is
the enabling foundation for file-as-reference; the storage model itself is
unchanged in this PR.
1 change: 1 addition & 0 deletions content/docs/references/system/object-storage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const result = AccessControlConfig.parse(data);
| **lastModified** | `string` | ✅ | Last modified timestamp |
| **created** | `string` | ✅ | Creation timestamp |
| **etag** | `string` | optional | Entity tag |
| **fileId** | `string` | optional | Opaque sys_file id (ADR-0104 D3 file-as-reference) |


---
Expand Down
11 changes: 9 additions & 2 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2048,8 +2048,15 @@ export class ObjectStackClient {
method: 'POST',
body: JSON.stringify(completeReq)
});

return completeRes.json();

// Surface the opaque sys_file id so the caller can store it in a file
// field (ADR-0104 D3). The server now returns it; fall back to the
// presigned id for an older server that does not.
const completeJson = (await completeRes.json()) as FileUploadResponse;
if (completeJson?.data && completeJson.data.fileId == null) {
completeJson.data.fileId = presigned.fileId;
}
return completeJson;
},

getDownloadUrl: async (fileId: string): Promise<string> => {
Expand Down
3 changes: 3 additions & 0 deletions packages/services/service-storage/src/storage-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ describe('Storage REST Routes', () => {
expect(completeRes._status).toBe(200);
expect(completeRes._json.data.name).toBe('test.txt');
expect(completeRes._json.data.mimeType).toBe('text/plain');
// ADR-0104 D3 PR-1: the commit response surfaces the opaque sys_file id
// so a caller can persist it in a file field.
expect(completeRes._json.data.fileId).toBe(fileId);
});

it('should 404 for unknown fileId', async () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/services/service-storage/src/storage-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ export function registerStorageRoutes(

res.json({
data: {
// The opaque sys_file id — the value a file field stores as a
// reference (ADR-0104 D3). Previously omitted, so a caller could not
// learn the id to persist after committing an upload.
fileId: updated!.id ?? fileId,
path: updated!.key,
name: updated!.name,
size: updated!.size ?? 0,
Expand Down
5 changes: 5 additions & 0 deletions packages/spec/src/system/object-storage.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export const FileMetadataSchema = lazySchema(() => z.object({
lastModified: z.string().datetime().describe('Last modified timestamp'),
created: z.string().datetime().describe('Creation timestamp'),
etag: z.string().optional().describe('Entity tag'),
// The opaque `sys_file` id (ADR-0104 D3): the value a file/image/avatar/
// video/audio field will store as a reference. Surfaced on the upload-complete
// response so a caller can persist it; optional because generic file-metadata
// reads (getInfo/list) do not always carry it.
fileId: z.string().optional().describe('Opaque sys_file id (ADR-0104 D3 file-as-reference)'),
}));

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