Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/blob-upload-filename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@thatopen/services': patch
---

`createFile` and `updateFile` (when a name is given) upload a bare `Blob` under the item's name, so the item gets its `fileExtension` instead of an empty one.
29 changes: 29 additions & 0 deletions src/core/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,35 @@ describe('EngineServicesClient — HTTP contract', () => {
});
});

describe('upload filename', () => {
// The platform takes fileExtension from the multipart filename; a bare
// Blob would be sent as "blob" and stored with fileExtension "".
it('createFile sends a bare Blob under the item name', async () => {
fetchMock.mockResolvedValue(okResponse({}));
const client = new EngineServicesClient(TOKEN, API);
await client.createFile({ file: new Blob(['{}']), name: 'result.json', versionTag: 'v1' });
const file = (getCall(fetchMock).init.body as FormData).get('file') as File;
expect(file.name).toBe('result.json');
expect(await file.text()).toBe('{}');
});

it('createFile keeps a File\'s own name', async () => {
fetchMock.mockResolvedValue(okResponse({}));
const client = new EngineServicesClient(TOKEN, API);
await client.createFile({ file: new File(['x'], 'model.ifc'), name: 'Model', versionTag: 'v1' });
const file = (getCall(fetchMock).init.body as FormData).get('file') as File;
expect(file.name).toBe('model.ifc');
});

it('updateFile names a bare Blob when a name is given', async () => {
fetchMock.mockResolvedValue(okResponse({}));
const client = new EngineServicesClient(TOKEN, API);
await client.updateFile('item-1', { file: new Blob(['{}']), name: 'result.json', versionTag: 'v2' });
const file = (getCall(fetchMock).init.body as FormData).get('file') as File;
expect(file.name).toBe('result.json');
});
});

describe('error handling', () => {
it('throws when the server responds with a non-2xx status', async () => {
fetchMock.mockResolvedValue(errorResponse(403, 'Forbidden'));
Expand Down
19 changes: 17 additions & 2 deletions src/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ export type EngineServicesClientProps = {
* const files = await client.listFiles();
* ```
*/
/**
* The platform takes a file's extension from the upload's filename, and a bare
* Blob is sent as "blob". Give it the item's name so `fileExtension` is set.
*/
function namedFile(file: File | Blob, name: string): File | Blob {
return file instanceof File ? file : new File([file], name, { type: file.type });
}

export class EngineServicesClient {
private apiUrl: string;
private accessToken: string;
Expand Down Expand Up @@ -557,7 +565,10 @@ export class EngineServicesClient {
* @returns The created item and its first version.
*/
async createFile(fileData: CreateItemProps) {
return await this.#createItem(fileData, ITEM_TYPE_FILE);
return await this.#createItem(
{ ...fileData, file: namedFile(fileData.file, fileData.name) },
ITEM_TYPE_FILE,
);
}

/**
Expand All @@ -571,7 +582,11 @@ export class EngineServicesClient {
fileId: string,
fileData: UpdateItemProps,
): Promise<UpdateItemResponse> {
return await this.#updateItem(fileId, fileData);
const { file, name } = fileData;
return await this.#updateItem(
fileId,
file && name ? { ...fileData, file: namedFile(file, name) } : fileData,
);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/core/examples/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ async function main() {
// projectId is required — the backend rejects uploads without one.
const fileName = `__example-test__-${Date.now()}.json`;
const blob = new Blob([JSON.stringify({ test: true })], { type: 'application/json' });
// A bare Blob is uploaded under `name`, so the item takes its fileExtension from it.
const created = await client.createFile({ file: blob, name: fileName, versionTag: 'v1', projectId: PROJECT_ID });
console.log(`\nCreated file: [${created.item._id}] ${created.item.name}`);

Expand Down