From 7a36371cf92230f2ee482eea95c400920c6b46e7 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 7 Oct 2026 13:13:33 -0500 Subject: [PATCH] fix: override inaccurate type for file coming from API spec --- __tests__/file-uploads.test.ts | 2 -- __tests__/multipart.test.ts | 60 ++++++++++++++++++++++++++++++++-- src/StreamChannel.ts | 24 +++++++++++++- src/StreamChatClient.ts | 30 ++++++++++++++++- 4 files changed, 110 insertions(+), 6 deletions(-) diff --git a/__tests__/file-uploads.test.ts b/__tests__/file-uploads.test.ts index e5f6425..14256c2 100644 --- a/__tests__/file-uploads.test.ts +++ b/__tests__/file-uploads.test.ts @@ -94,7 +94,6 @@ describe.skip('channel file uploads', () => { const fileBuffer = fs.readFileSync(filePath); const response = await channel.uploadChannelFile({ - // @ts-expect-error API spec says file should be a string file: new File([fileBuffer], 'test-file.pdf'), user: { id: user.id }, }); @@ -122,7 +121,6 @@ describe.skip('channel file uploads', () => { ]; const response = await channel.uploadChannelImage({ - // @ts-expect-error API spec says file should be a string file: new File([fileBuffer], 'test-image.jpg'), user: { id: user.id }, upload_sizes: uploadSizes, diff --git a/__tests__/multipart.test.ts b/__tests__/multipart.test.ts index 9012714..114f73e 100644 --- a/__tests__/multipart.test.ts +++ b/__tests__/multipart.test.ts @@ -125,7 +125,6 @@ describe('multipart form data serialization', () => { await chat.uploadChannelImage({ type: 'messaging', id: 'channel-id', - // @ts-expect-error API spec says file should be a string file: new File(['image-contents'], 'test-image.jpg'), upload_sizes: [{ width: 100, height: 100 }], user: { id: 'user-id' }, @@ -144,7 +143,6 @@ describe('multipart form data serialization', () => { await chat.uploadChannelFile({ type: 'messaging', id: 'channel-id', - // @ts-expect-error API spec says file should be a string file: new File(['file-contents'], 'test-file.pdf'), user: { id: 'user-id' }, }); @@ -153,4 +151,62 @@ describe('multipart form data serialization', () => { expect(formData.get('user')).toBe('{"id":"user-id"}'); expect(formData.get('file')).toBeInstanceOf(File); }); + + it('encodes a channel image upload made from a channel', async () => { + const fetchSpy = mockFetch(); + const channel = new StreamChatClient(createApiClient()).channel( + 'messaging', + 'channel-id', + ); + + await channel.uploadChannelImage({ + file: new File(['image-contents'], 'test-image.jpg'), + upload_sizes: [{ width: 100, height: 100 }], + user: { id: 'user-id' }, + }); + + const [url, init] = fetchSpy.mock.calls[0]; + expect(url).toContain('/api/v2/chat/channels/messaging/channel-id/image'); + const formData = init!.body as FormData; + expect(formData.get('user')).toBe('{"id":"user-id"}'); + expect(formData.get('upload_sizes')).toBe('[{"width":100,"height":100}]'); + const sent = formData.get('file'); + expect(sent).toBeInstanceOf(File); + expect((sent as File).name).toBe('test-image.jpg'); + }); + + it('encodes a channel file upload made from a channel', async () => { + const fetchSpy = mockFetch(); + const channel = new StreamChatClient(createApiClient()).channel( + 'messaging', + 'channel-id', + ); + + await channel.uploadChannelFile({ + file: new File(['file-contents'], 'test-file.pdf'), + user: { id: 'user-id' }, + }); + + const [url, init] = fetchSpy.mock.calls[0]; + expect(url).toContain('/api/v2/chat/channels/messaging/channel-id/file'); + const formData = init!.body as FormData; + expect(formData.get('user')).toBe('{"id":"user-id"}'); + const sent = formData.get('file'); + expect(sent).toBeInstanceOf(File); + expect((sent as File).name).toBe('test-file.pdf'); + }); + + it('refuses a channel upload before the channel has an id', () => { + const fetchSpy = mockFetch(); + const channel = new StreamChatClient(createApiClient()).channel( + 'messaging', + ); + + expect(() => + channel.uploadChannelFile({ + file: new File(['file-contents'], 'test-file.pdf'), + }), + ).toThrow("Channel isn't yet created"); + expect(fetchSpy).not.toHaveBeenCalled(); + }); }); diff --git a/src/StreamChannel.ts b/src/StreamChannel.ts index ef669f0..44f8bd9 100644 --- a/src/StreamChannel.ts +++ b/src/StreamChannel.ts @@ -1,6 +1,12 @@ import { ChannelApi } from './gen/chat/ChannelApi'; -import { ChannelGetOrCreateRequest, QueryMembersPayload } from './gen/models'; +import { + ChannelGetOrCreateRequest, + QueryMembersPayload, + UploadChannelFileRequest, + UploadChannelRequest, +} from './gen/models'; import { OmitTypeId } from './types'; +import { File } from 'buffer'; export class StreamChannel extends ChannelApi { get cid() { @@ -36,4 +42,20 @@ export class StreamChannel extends ChannelApi { }, }); } + + // @ts-expect-error API spec says file should be a string + uploadChannelFile = ( + request: Omit & { file: File }, + ) => { + // @ts-expect-error API spec says file should be a string + return super.uploadChannelFile(request); + }; + + // @ts-expect-error API spec says file should be a string + uploadChannelImage = ( + request: Omit & { file: File }, + ) => { + // @ts-expect-error API spec says file should be a string + return super.uploadChannelImage(request); + }; } diff --git a/src/StreamChatClient.ts b/src/StreamChatClient.ts index dc82a27..532451e 100644 --- a/src/StreamChatClient.ts +++ b/src/StreamChatClient.ts @@ -1,8 +1,36 @@ import { ChatApi } from './gen/chat/ChatApi'; +import { UploadChannelFileRequest, UploadChannelRequest } from './gen/models'; import { StreamChannel } from './StreamChannel'; +import { File } from 'buffer'; export class StreamChatClient extends ChatApi { channel = (type: string, id?: string) => { - return new StreamChannel(this, type, id); + // The upload overrides below narrow `file` to File, so this class is no + // longer structurally a ChatApi, even though it is one at runtime + return new StreamChannel(this as unknown as ChatApi, type, id); + }; + + // @ts-expect-error API spec says file should be a string + uploadChannelFile = ( + request: Omit & { + file: File; + type: string; + id: string; + }, + ) => { + // @ts-expect-error API spec says file should be a string + return super.uploadChannelFile(request); + }; + + // @ts-expect-error API spec says file should be a string + uploadChannelImage = ( + request: Omit & { + file: File; + type: string; + id: string; + }, + ) => { + // @ts-expect-error API spec says file should be a string + return super.uploadChannelImage(request); }; }