Skip to content

Commit aa43a6b

Browse files
szuperazkmitrovvitsmeadi
authored
feat!: add support for feeds v3 API (#118)
Co-authored-by: Kire Mitrov <[email protected]> Co-authored-by: Aditya Agarwal <[email protected]>
1 parent 3ade594 commit aa43a6b

23 files changed

+10504
-3786
lines changed

CHANGELOG.md

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,69 @@
22

33
## [0.4.26](https://github.com/GetStream/stream-node/compare/v0.4.25...v0.4.26) (2025-07-16)
44

5-
65
### Features
76

8-
* Update to API spec 186.1.0 ([#120](https://github.com/GetStream/stream-node/issues/120)) ([e1d1f88](https://github.com/GetStream/stream-node/commit/e1d1f88d823f71d6386a1791e285bdbee2cbfb54))
7+
- Update to API spec 186.1.0 ([#120](https://github.com/GetStream/stream-node/issues/120)) ([e1d1f88](https://github.com/GetStream/stream-node/commit/e1d1f88d823f71d6386a1791e285bdbee2cbfb54))
98

109
## [0.4.25](https://github.com/GetStream/stream-node/compare/v0.4.24...v0.4.25) (2025-06-24)
1110

12-
1311
### Features
1412

15-
* Update to API spec v179.7.0 ([#113](https://github.com/GetStream/stream-node/issues/113)) ([898ee9e](https://github.com/GetStream/stream-node/commit/898ee9e692b85075e60147835ff43de3f1179587))
13+
- Update to API spec v179.7.0 ([#113](https://github.com/GetStream/stream-node/issues/113)) ([898ee9e](https://github.com/GetStream/stream-node/commit/898ee9e692b85075e60147835ff43de3f1179587))
1614

1715
## [0.4.24](https://github.com/GetStream/stream-node/compare/v0.4.23...v0.4.24) (2025-05-06)
1816

19-
2017
### Features
2118

22-
* empty commit to trigger release ([c0890a8](https://github.com/GetStream/stream-node/commit/c0890a8d8b7adc96d4d81717220159321e924480))
19+
- empty commit to trigger release ([c0890a8](https://github.com/GetStream/stream-node/commit/c0890a8d8b7adc96d4d81717220159321e924480))
2320

2421
## [0.4.23](https://github.com/GetStream/stream-node/compare/v0.4.22...v0.4.23) (2025-04-30)
2522

26-
2723
### Features
2824

29-
* update to v171.1.7 ([#107](https://github.com/GetStream/stream-node/issues/107)) ([dd442f0](https://github.com/GetStream/stream-node/commit/dd442f0e6b9acc465781cb748e5c5a03dd51624c))
25+
- update to v171.1.7 ([#107](https://github.com/GetStream/stream-node/issues/107)) ([dd442f0](https://github.com/GetStream/stream-node/commit/dd442f0e6b9acc465781cb748e5c5a03dd51624c))
3026

3127
## [0.4.22](https://github.com/GetStream/stream-node/compare/v0.4.21...v0.4.22) (2025-04-09)
3228

33-
3429
### Bug Fixes
3530

36-
* handle missing optional dependency in declarations ([#103](https://github.com/GetStream/stream-node/issues/103)) ([eb271c5](https://github.com/GetStream/stream-node/commit/eb271c5f3bfb6002d341323af25beabd15516ef2))
31+
- handle missing optional dependency in declarations ([#103](https://github.com/GetStream/stream-node/issues/103)) ([eb271c5](https://github.com/GetStream/stream-node/commit/eb271c5f3bfb6002d341323af25beabd15516ef2))
3732

3833
## [0.4.21](https://github.com/GetStream/stream-node/compare/v0.4.20...v0.4.21) (2025-04-08)
3934

40-
4135
### Bug Fixes
4236

43-
* remove undici dependency ([#101](https://github.com/GetStream/stream-node/issues/101)) ([c943be1](https://github.com/GetStream/stream-node/commit/c943be1b2871b2b010dd4b29d690c46ecacda23b))
37+
- remove undici dependency ([#101](https://github.com/GetStream/stream-node/issues/101)) ([c943be1](https://github.com/GetStream/stream-node/commit/c943be1b2871b2b010dd4b29d690c46ecacda23b))
4438

4539
## [0.4.20](https://github.com/GetStream/stream-node/compare/v0.4.19...v0.4.20) (2025-04-07)
4640

47-
4841
### Features
4942

50-
* set max connections to 100, allow integrators to configure Fetch API ([#98](https://github.com/GetStream/stream-node/issues/98)) ([b044b59](https://github.com/GetStream/stream-node/commit/b044b599867a69b33b3aa9d989d1c1e3277dc92f))
43+
- set max connections to 100, allow integrators to configure Fetch API ([#98](https://github.com/GetStream/stream-node/issues/98)) ([b044b59](https://github.com/GetStream/stream-node/commit/b044b599867a69b33b3aa9d989d1c1e3277dc92f))
5144

5245
## [0.4.19](https://github.com/GetStream/stream-node/compare/v0.4.18...v0.4.19) (2025-03-17)
5346

54-
5547
### Features
5648

57-
* update to API spec v163.0.0 ([#96](https://github.com/GetStream/stream-node/issues/96)) ([dcdfea8](https://github.com/GetStream/stream-node/commit/dcdfea8cfbf83b0ef3426426909c844424f682ab))
49+
- update to API spec v163.0.0 ([#96](https://github.com/GetStream/stream-node/issues/96)) ([dcdfea8](https://github.com/GetStream/stream-node/commit/dcdfea8cfbf83b0ef3426426909c844424f682ab))
5850

5951
## [0.4.18](https://github.com/GetStream/stream-node/compare/v0.4.17...v0.4.18) (2025-03-10)
6052

61-
6253
### Bug Fixes
6354

64-
* support realtime AI model overrides ([#94](https://github.com/GetStream/stream-node/issues/94)) ([1071f75](https://github.com/GetStream/stream-node/commit/1071f75aaf7ffb029f8c3c4c06465e81abf341d6))
55+
- support realtime AI model overrides ([#94](https://github.com/GetStream/stream-node/issues/94)) ([1071f75](https://github.com/GetStream/stream-node/commit/1071f75aaf7ffb029f8c3c4c06465e81abf341d6))
6556

6657
## [0.4.17](https://github.com/GetStream/stream-node/compare/v0.4.16...v0.4.17) (2025-03-06)
6758

68-
6959
### Bug Fixes
7060

71-
* use an exact version of @stream-io/openai-realtime-api ([#92](https://github.com/GetStream/stream-node/issues/92)) ([d6f0419](https://github.com/GetStream/stream-node/commit/d6f0419f924bfdadbb44d2b12b44a664bb4b39a6))
61+
- use an exact version of @stream-io/openai-realtime-api ([#92](https://github.com/GetStream/stream-node/issues/92)) ([d6f0419](https://github.com/GetStream/stream-node/commit/d6f0419f924bfdadbb44d2b12b44a664bb4b39a6))
7262

7363
## [0.4.16](https://github.com/GetStream/stream-node/compare/v0.4.15...v0.4.16) (2025-02-25)
7464

75-
7665
### Bug Fixes
7766

78-
* update changelog ([#89](https://github.com/GetStream/stream-node/issues/89)) ([1b46c91](https://github.com/GetStream/stream-node/commit/1b46c919ccc5a98414ca441b833020743217e95d))
67+
- update changelog ([#89](https://github.com/GetStream/stream-node/issues/89)) ([1b46c91](https://github.com/GetStream/stream-node/commit/1b46c919ccc5a98414ca441b833020743217e95d))
7968

8069
## [0.4.15](https://github.com/GetStream/stream-node/compare/v0.4.14...v0.4.15) (2025-02-25)
8170

__tests__/assets/test-file.pdf

15.4 KB
Binary file not shown.

__tests__/devices-push.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ describe('devices and push', () => {
6262
expect(response.push_providers).toBeDefined();
6363
});
6464

65-
it('test push provider', async () => {
65+
// Stream error code 4: CheckPush failed with error: "User has no enabled devices associated"
66+
it.skip('test push provider', async () => {
6667
const response = await client.checkPush({ user_id: user.id });
6768

6869
expect(response).toBeDefined();

__tests__/file-uploads.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { beforeAll, describe, it, expect } from 'vitest';
2+
import { createTestClient } from './create-test-client';
3+
import { StreamClient } from '../src/StreamClient';
4+
import fs from 'fs';
5+
import path from 'path';
6+
import { File } from 'buffer';
7+
8+
// Don't want to upload files and image every time we run the tests
9+
describe.skip('global file uploads', () => {
10+
let client: StreamClient;
11+
const user = {
12+
id: 'stream-node-test-user',
13+
role: 'admin',
14+
};
15+
16+
beforeAll(async () => {
17+
client = createTestClient();
18+
await client.upsertUsers([user]);
19+
});
20+
21+
it('upload and delete file', async () => {
22+
// Read the test PDF file from assets
23+
const filePath = path.join(__dirname, 'assets', 'test-file.pdf');
24+
const fileBuffer = fs.readFileSync(filePath);
25+
26+
const response = await client.uploadFile({
27+
file: new File([fileBuffer], 'test-file.pdf'),
28+
user: { id: user.id },
29+
});
30+
31+
expect(response).toBeDefined();
32+
expect(response.file).toBeDefined();
33+
expect(response.duration).toBeDefined();
34+
35+
const deleteResponse = await client.deleteFile({
36+
url: response.file,
37+
});
38+
39+
expect(deleteResponse).toBeDefined();
40+
});
41+
42+
it('upload image', async () => {
43+
// Read the test PDF file from assets
44+
const filePath = path.join(__dirname, 'assets', 'test-image.jpg');
45+
const fileBuffer = fs.readFileSync(filePath);
46+
47+
const uploadSizes = [
48+
{
49+
width: 100,
50+
height: 100,
51+
resize: 'scale',
52+
crop: 'center',
53+
},
54+
];
55+
56+
// Upload the file
57+
const response = await client.uploadImage({
58+
file: new File([fileBuffer], 'test-image.jpg'),
59+
user: { id: user.id },
60+
upload_sizes: uploadSizes,
61+
});
62+
63+
expect(response.upload_sizes?.length).toBe(1);
64+
expect(response.upload_sizes?.[0]).toMatchObject(uploadSizes[0]);
65+
const deleteResponse = await client.deleteImage({
66+
url: response.file,
67+
});
68+
69+
expect(deleteResponse).toBeDefined();
70+
});
71+
});

generate-openapi.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ CHAT_DIR="../chat"
66

77
rm -rf $OUTPUT_DIR
88

9-
( cd $CHAT_DIR ; make openapi ; go run ./cmd/chat-manager openapi generate-client --language ts --spec ./releases/v2/serverside-api.yaml --output $OUTPUT_DIR )
9+
( cd $CHAT_DIR ; make openapi ; go run ./cmd/chat-manager openapi generate-client --language ts --spec ./releases/v2/feeds-serverside-api.yaml --output $OUTPUT_DIR )
1010

1111
yarn lint:gen

index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
export * from './src/gen/models';
21
export * from './src/StreamClient';
32
export * from './src/StreamCall';
43
export * from './src/StreamChatClient';
54
export * from './src/StreamChannel';
65
export * from './src/StreamVideoClient';
6+
export * from './src/gen/models';
7+
export * from './src/StreamFeedsClient';
8+
export * from './src/StreamFeed';

src/BaseApi.ts renamed to src/ApiClient.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@ import { ApiConfig, RequestMetadata, StreamError } from './types';
33
import { APIError } from './gen/models';
44
import { getRateLimitFromResponseHeader } from './utils/rate-limit';
55

6-
export class BaseApi {
6+
export class ApiClient {
77
private readonly dispatcher?: RequestInit['dispatcher'];
88

9-
constructor(protected readonly apiConfig: ApiConfig) {
9+
constructor(public readonly apiConfig: ApiConfig) {
1010
this.dispatcher = this.apiConfig.agent;
1111
}
1212

13-
protected sendRequest = async <T>(
13+
/**
14+
*
15+
* @internal
16+
*/
17+
sendRequest = async <T>(
1418
method: string,
1519
url: string,
1620
pathParams?: Record<string, string>,
1721
queryParams?: Record<string, any>,
1822
body?: any,
23+
requestContentType?: string,
1924
) => {
2025
queryParams = queryParams ?? {};
2126
queryParams.api_key = this.apiConfig.apiKey;
@@ -28,22 +33,36 @@ export class BaseApi {
2833

2934
url += `?${encodedParams}`;
3035
const clientRequestId = uuidv4();
31-
const headers = {
36+
const headers: Record<string, string> = {
3237
Authorization: this.apiConfig.token,
3338
'stream-auth-type': 'jwt',
34-
'Content-Type': 'application/json',
3539
'X-Stream-Client': 'stream-node-' + process.env.PKG_VERSION,
3640
'Accept-Encoding': 'gzip',
3741
'x-client-request-id': clientRequestId,
3842
};
3943

44+
// https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post
45+
if (requestContentType !== 'multipart/form-data') {
46+
headers['Content-Type'] = requestContentType ?? 'application/json';
47+
}
48+
4049
const signal = AbortSignal.timeout(this.apiConfig.timeout);
4150

51+
const encodedBody =
52+
requestContentType === 'multipart/form-data'
53+
? new FormData()
54+
: JSON.stringify(body);
55+
if (requestContentType === 'multipart/form-data') {
56+
Object.keys(body).forEach((key) => {
57+
(encodedBody as FormData).append(key, body[key]);
58+
});
59+
}
60+
4261
try {
4362
const response = await fetch(`${this.apiConfig.baseUrl}${url}`, {
4463
signal,
4564
method,
46-
body: JSON.stringify(body),
65+
body: encodedBody,
4766
headers,
4867
dispatcher: this.dispatcher,
4968
});

src/StreamCall.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { QueryCallMembersRequest } from './gen/models';
1+
import { GetOrCreateCallRequest, QueryCallMembersRequest } from './gen/models';
22
import { CallApi } from './gen/video/CallApi';
33
import { OmitTypeId } from './types';
44

@@ -7,7 +7,7 @@ export class StreamCall extends CallApi {
77
return `${this.type}:${this.id}`;
88
}
99

10-
create = this.getOrCreate;
10+
create = (request?: GetOrCreateCallRequest) => this.getOrCreate(request);
1111

1212
queryMembers = (request?: OmitTypeId<QueryCallMembersRequest>) => {
1313
return this.videoApi.queryCallMembers({

src/StreamClient.ts

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ import { StreamVideoClient } from './StreamVideoClient';
44
import crypto from 'crypto';
55
import { StreamChatClient } from './StreamChatClient';
66
import { CallTokenPayload, UserTokenPayload } from './types';
7-
import { QueryBannedUsersPayload, UserRequest } from './gen/models';
7+
import {
8+
FileUploadRequest,
9+
ImageUploadRequest,
10+
QueryBannedUsersPayload,
11+
UserRequest,
12+
} from './gen/models';
813
import { StreamModerationClient } from './StreamModerationClient';
14+
import { ApiClient } from './ApiClient';
15+
import { StreamFeedsClient } from './StreamFeedsClient';
16+
import { File } from 'buffer';
917

1018
export interface StreamClientOptions {
1119
timeout?: number;
@@ -19,6 +27,7 @@ export class StreamClient extends CommonApi {
1927
public readonly video: StreamVideoClient;
2028
public readonly chat: StreamChatClient;
2129
public readonly moderation: StreamModerationClient;
30+
public readonly feeds: StreamFeedsClient;
2231
public readonly options: StreamClientOptions = {};
2332

2433
private static readonly DEFAULT_TIMEOUT = 3000;
@@ -38,36 +47,40 @@ export class StreamClient extends CommonApi {
3847
const timeout = config?.timeout ?? StreamClient.DEFAULT_TIMEOUT;
3948
const chatBaseUrl = config?.basePath ?? 'https://chat.stream-io-api.com';
4049
const videoBaseUrl = config?.basePath ?? 'https://video.stream-io-api.com';
41-
super({
50+
const feedsBaseUrl = config?.basePath ?? 'https://video.stream-io-api.com';
51+
const chatApiClient = new ApiClient({
4252
apiKey,
4353
token,
44-
timeout,
4554
baseUrl: chatBaseUrl,
55+
timeout,
4656
agent: config?.agent as RequestInit['dispatcher'],
4757
});
4858

49-
this.video = new StreamVideoClient({
50-
streamClient: this,
59+
const videoApiClient = new ApiClient({
5160
apiKey,
5261
token,
53-
timeout,
5462
baseUrl: videoBaseUrl,
55-
agent: config?.agent as RequestInit['dispatcher'],
56-
});
57-
this.chat = new StreamChatClient({
58-
apiKey,
59-
token,
6063
timeout,
61-
baseUrl: chatBaseUrl,
6264
agent: config?.agent as RequestInit['dispatcher'],
6365
});
64-
this.moderation = new StreamModerationClient({
66+
67+
const feedsApiClient = new ApiClient({
6568
apiKey,
6669
token,
70+
baseUrl: feedsBaseUrl,
6771
timeout,
68-
baseUrl: chatBaseUrl,
6972
agent: config?.agent as RequestInit['dispatcher'],
7073
});
74+
75+
super(chatApiClient);
76+
77+
this.video = new StreamVideoClient({
78+
streamClient: this,
79+
apiClient: videoApiClient,
80+
});
81+
this.chat = new StreamChatClient(this.apiClient);
82+
this.moderation = new StreamModerationClient(chatApiClient);
83+
this.feeds = new StreamFeedsClient(feedsApiClient);
7184
}
7285

7386
upsertUsers = (users: UserRequest[]) => {
@@ -84,6 +97,30 @@ export class StreamClient extends CommonApi {
8497
return this.chat.queryBannedUsers(request);
8598
};
8699

100+
// @ts-expect-error API spec says file should be a string
101+
uploadFile = (request: Omit<FileUploadRequest, 'file'> & { file: File }) => {
102+
return super.uploadFile({
103+
// @ts-expect-error API spec says file should be a string
104+
file: request.file,
105+
// @ts-expect-error form data will only work if this is a string
106+
user: JSON.stringify(request.user),
107+
});
108+
};
109+
110+
// @ts-expect-error API spec says file should be a string
111+
uploadImage = (
112+
request: Omit<ImageUploadRequest, 'file'> & { file: File },
113+
) => {
114+
return super.uploadImage({
115+
// @ts-expect-error API spec says file should be a string
116+
file: request.file,
117+
// @ts-expect-error form data will only work if this is a string
118+
user: JSON.stringify(request.user),
119+
// @ts-expect-error form data will only work if this is a string
120+
upload_sizes: JSON.stringify(request.upload_sizes),
121+
});
122+
};
123+
87124
/**
88125
*
89126
* @param payload

src/StreamFeed.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { FeedApi } from './gen/feeds/FeedApi';
2+
3+
export class StreamFeed extends FeedApi {
4+
get fid() {
5+
return `${this.group}:${this.id}`;
6+
}
7+
}

0 commit comments

Comments
 (0)