diff --git a/.changeset/adr-0104-d3w2-pr1-upload-complete-fileid.md b/.changeset/adr-0104-d3w2-pr1-upload-complete-fileid.md new file mode 100644 index 0000000000..4ddd916b06 --- /dev/null +++ b/.changeset/adr-0104-d3w2-pr1-upload-complete-fileid.md @@ -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. diff --git a/content/docs/references/system/object-storage.mdx b/content/docs/references/system/object-storage.mdx index 504fd7be79..2722573d24 100644 --- a/content/docs/references/system/object-storage.mdx +++ b/content/docs/references/system/object-storage.mdx @@ -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) | --- diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 3b50037cf9..335b3d0a2f 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -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 => { diff --git a/packages/services/service-storage/src/storage-routes.test.ts b/packages/services/service-storage/src/storage-routes.test.ts index 20fadbf000..ebd140f3f1 100644 --- a/packages/services/service-storage/src/storage-routes.test.ts +++ b/packages/services/service-storage/src/storage-routes.test.ts @@ -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 () => { diff --git a/packages/services/service-storage/src/storage-routes.ts b/packages/services/service-storage/src/storage-routes.ts index a547c3321d..77745b8799 100644 --- a/packages/services/service-storage/src/storage-routes.ts +++ b/packages/services/service-storage/src/storage-routes.ts @@ -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, diff --git a/packages/spec/src/system/object-storage.zod.ts b/packages/spec/src/system/object-storage.zod.ts index 9066899d00..8ef6c36715 100644 --- a/packages/spec/src/system/object-storage.zod.ts +++ b/packages/spec/src/system/object-storage.zod.ts @@ -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;