@scryme/chat 2.91.43 → 2.91.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,229 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import axios, { AxiosError } from 'axios';
3
+ import { ScrymeSDKError, ScrymeError, parseSDKError } from '../custom-instance';
4
+ import { ScrymeSDK } from '../sdk';
5
+
6
+ describe('ScrymeSDKError & parseSDKError', () => {
7
+ it('should construct ScrymeSDKError with status, code, data, and rawError', () => {
8
+ const rawErr = new Error('Raw');
9
+ const err = new ScrymeSDKError('Something went wrong', {
10
+ status: 400,
11
+ code: 'BAD_REQUEST',
12
+ data: { details: 'Invalid field' },
13
+ rawError: rawErr,
14
+ });
15
+
16
+ expect(err).toBeInstanceOf(Error);
17
+ expect(err).toBeInstanceOf(ScrymeSDKError);
18
+ expect(err).toBeInstanceOf(ScrymeError);
19
+ expect(err.name).toBe('ScrymeSDKError');
20
+ expect(err.message).toBe('Something went wrong');
21
+ expect(err.status).toBe(400);
22
+ expect(err.code).toBe('BAD_REQUEST');
23
+ expect(err.data).toEqual({ details: 'Invalid field' });
24
+ expect(err.rawError).toBe(rawErr);
25
+ });
26
+
27
+ it('should return existing ScrymeSDKError as is', () => {
28
+ const original = new ScrymeSDKError('Already parsed');
29
+ const result = parseSDKError(original);
30
+ expect(result).toBe(original);
31
+ });
32
+
33
+ it('should parse NestJS validation error arrays into joined message', () => {
34
+ const axiosError = new AxiosError(
35
+ 'Request failed with status code 400',
36
+ 'ERR_BAD_REQUEST',
37
+ undefined,
38
+ {},
39
+ {
40
+ status: 400,
41
+ statusText: 'Bad Request',
42
+ headers: {},
43
+ config: {} as any,
44
+ data: {
45
+ statusCode: 400,
46
+ error: 'Bad Request',
47
+ message: ['email must be an email', 'password is too short'],
48
+ },
49
+ }
50
+ );
51
+
52
+ const parsed = parseSDKError(axiosError);
53
+ expect(parsed).toBeInstanceOf(ScrymeSDKError);
54
+ expect(parsed.message).toBe('email must be an email; password is too short');
55
+ expect(parsed.status).toBe(400);
56
+ expect(parsed.code).toBe('ERR_BAD_REQUEST');
57
+ expect(parsed.data).toEqual({
58
+ statusCode: 400,
59
+ error: 'Bad Request',
60
+ message: ['email must be an email', 'password is too short'],
61
+ });
62
+ });
63
+
64
+ it('should parse standard NestJS error response with string message', () => {
65
+ const axiosError = new AxiosError(
66
+ 'Request failed with status code 404',
67
+ 'ERR_BAD_REQUEST',
68
+ undefined,
69
+ {},
70
+ {
71
+ status: 404,
72
+ statusText: 'Not Found',
73
+ headers: {},
74
+ config: {} as any,
75
+ data: {
76
+ statusCode: 404,
77
+ error: 'Not Found',
78
+ message: "Workspace 'acme' not found",
79
+ },
80
+ }
81
+ );
82
+
83
+ const parsed = parseSDKError(axiosError);
84
+ expect(parsed.message).toBe("Workspace 'acme' not found");
85
+ expect(parsed.status).toBe(404);
86
+ });
87
+
88
+ it('should parse error object format { error: "Unauthorized" }', () => {
89
+ const axiosError = new AxiosError(
90
+ 'Request failed with status code 401',
91
+ 'ERR_BAD_REQUEST',
92
+ undefined,
93
+ {},
94
+ {
95
+ status: 401,
96
+ statusText: 'Unauthorized',
97
+ headers: {},
98
+ config: {} as any,
99
+ data: {
100
+ error: 'Invalid session token',
101
+ },
102
+ }
103
+ );
104
+
105
+ const parsed = parseSDKError(axiosError);
106
+ expect(parsed.message).toBe('Invalid session token');
107
+ expect(parsed.status).toBe(401);
108
+ });
109
+
110
+ it('should parse nested error object format { error: { message: "Token expired" } }', () => {
111
+ const axiosError = new AxiosError(
112
+ 'Request failed with status code 401',
113
+ 'ERR_BAD_REQUEST',
114
+ undefined,
115
+ {},
116
+ {
117
+ status: 401,
118
+ statusText: 'Unauthorized',
119
+ headers: {},
120
+ config: {} as any,
121
+ data: {
122
+ error: {
123
+ message: 'Token expired',
124
+ code: 'TOKEN_EXPIRED',
125
+ },
126
+ },
127
+ }
128
+ );
129
+
130
+ const parsed = parseSDKError(axiosError);
131
+ expect(parsed.message).toBe('Token expired');
132
+ expect(parsed.status).toBe(401);
133
+ });
134
+
135
+ it('should parse string response data', () => {
136
+ const axiosError = new AxiosError(
137
+ 'Request failed with status code 502',
138
+ 'ERR_BAD_GATEWAY',
139
+ undefined,
140
+ {},
141
+ {
142
+ status: 502,
143
+ statusText: 'Bad Gateway',
144
+ headers: {},
145
+ config: {} as any,
146
+ data: 'Bad Gateway Error from Upstream',
147
+ }
148
+ );
149
+
150
+ const parsed = parseSDKError(axiosError);
151
+ expect(parsed.message).toBe('Bad Gateway Error from Upstream');
152
+ expect(parsed.status).toBe(502);
153
+ });
154
+
155
+ it('should handle network errors without response object', () => {
156
+ const axiosError = new AxiosError(
157
+ 'Network Error',
158
+ 'ERR_NETWORK',
159
+ undefined,
160
+ {},
161
+ undefined
162
+ );
163
+
164
+ const parsed = parseSDKError(axiosError);
165
+ expect(parsed.message).toBe('Network Error');
166
+ expect(parsed.status).toBeUndefined();
167
+ expect(parsed.code).toBe('ERR_NETWORK');
168
+ });
169
+
170
+ it('should handle timeout errors without response object', () => {
171
+ const axiosError = new AxiosError(
172
+ 'timeout of 10000ms exceeded',
173
+ 'ECONNABORTED',
174
+ undefined,
175
+ {},
176
+ undefined
177
+ );
178
+
179
+ const parsed = parseSDKError(axiosError);
180
+ expect(parsed.message).toBe('timeout of 10000ms exceeded');
181
+ expect(parsed.code).toBe('ECONNABORTED');
182
+ });
183
+
184
+ it('should handle standard non-Axios Error objects', () => {
185
+ const standardError = new TypeError('Failed to fetch');
186
+ const parsed = parseSDKError(standardError);
187
+
188
+ expect(parsed).toBeInstanceOf(ScrymeSDKError);
189
+ expect(parsed.message).toBe('Failed to fetch');
190
+ });
191
+
192
+ it('should handle primitive string errors', () => {
193
+ const parsed = parseSDKError('Custom error string');
194
+
195
+ expect(parsed).toBeInstanceOf(ScrymeSDKError);
196
+ expect(parsed.message).toBe('Custom error string');
197
+ });
198
+
199
+ it('should wrap M2M token fetch error into ScrymeSDKError', async () => {
200
+ const sdk = new ScrymeSDK({
201
+ baseURL: 'https://api.test.com',
202
+ clientId: 'invalid-id',
203
+ clientSecret: 'invalid-secret',
204
+ });
205
+
206
+ // Mock axios post failure
207
+ const postSpy = vi.spyOn(axios, 'post').mockRejectedValue(
208
+ new AxiosError('Request failed with status code 401', 'ERR_BAD_REQUEST', undefined, {}, {
209
+ status: 401,
210
+ statusText: 'Unauthorized',
211
+ headers: {},
212
+ config: {} as any,
213
+ data: { message: 'Invalid M2M client credentials' },
214
+ })
215
+ );
216
+
217
+ await expect(sdk.getOrFetchToken()).rejects.toThrow(ScrymeSDKError);
218
+
219
+ try {
220
+ await sdk.getOrFetchToken();
221
+ } catch (err: any) {
222
+ expect(err).toBeInstanceOf(ScrymeSDKError);
223
+ expect(err.message).toBe('Invalid M2M client credentials');
224
+ expect(err.status).toBe(401);
225
+ }
226
+
227
+ postSpy.mockRestore();
228
+ });
229
+ });
@@ -48,7 +48,35 @@ vi.mock('../generated/v3-server', () => {
48
48
  return { success: true, type: 'channel', action: 'removeReaction', channelId, messageId, emoji, options };
49
49
  }),
50
50
 
51
- // DMs Mock
51
+ // V3 Channels Controller Messages Mock
52
+ v3WorkspacesControllerGetChannelMessages: vi.fn(async (slug, channelId, params, options) => {
53
+ return { success: true, data: { messages: [] }, slug, channelId, params, options };
54
+ }),
55
+ v3WorkspacesControllerCreateChannelMessage: vi.fn(async (slug, channelId, options) => {
56
+ return { success: true, data: { message: { content: options?.data?.content } }, slug, channelId, options };
57
+ }),
58
+
59
+ // V3 DMs Mock
60
+ v3DmsControllerGetDms: vi.fn(async (options) => {
61
+ return { success: true, data: { conversations: [] }, options };
62
+ }),
63
+ v3DmsControllerCreateDm: vi.fn(async (data, options) => {
64
+ return { success: true, data: { conversation: { data } }, options };
65
+ }),
66
+ v3DmsControllerGetDm: vi.fn(async (dmId, options) => {
67
+ return { success: true, data: { conversation: { id: dmId } }, options };
68
+ }),
69
+ v3DmsControllerDeleteDm: vi.fn(async (dmId, options) => {
70
+ return { success: true, data: { success: true }, dmId, options };
71
+ }),
72
+ v3DmsControllerGetMessages: vi.fn(async (dmId, params, options) => {
73
+ return { success: true, data: { messages: [] }, dmId, params, options };
74
+ }),
75
+ v3DmsControllerCreateMessage: vi.fn(async (dmId, options) => {
76
+ return { success: true, data: { message: { content: options?.data?.content } }, dmId, options };
77
+ }),
78
+
79
+ // Legacy DMs Mock
52
80
  dmsControllerGetDms: vi.fn(async (options) => {
53
81
  return { success: true, conversations: [], options };
54
82
  }),
@@ -423,18 +451,16 @@ describe('ScrymeSDK', () => {
423
451
  token: 'active-token',
424
452
  });
425
453
 
426
- const messagesRes = await sdk.channel.message.list('chan_123', { limit: 10 }) as any;
427
- expect(messagesRes.channelId).toBe('chan_123');
428
- expect(messagesRes.params.limit).toBe(10);
454
+ const messagesRes = await sdk.channel.message.list('acme-corp', 'chan_123', { limit: '10' }) as any;
455
+ expect(messagesRes.messages).toBeDefined();
429
456
 
430
457
  // Channel message create (with string)
431
- const createMsgStrRes = await sdk.channel.message.create('chan_123', 'Hello') as any;
432
- expect(createMsgStrRes.channelId).toBe('chan_123');
433
- expect(createMsgStrRes.options.data.content).toBe('Hello');
458
+ const createMsgStrRes = await sdk.channel.message.create('acme-corp', 'chan_123', 'Hello') as any;
459
+ expect(createMsgStrRes.content).toBe('Hello');
434
460
 
435
461
  // Channel message create (with object)
436
- const createMsgObjRes = await sdk.channel.message.create('chan_123', { content: 'Hello World' }) as any;
437
- expect(createMsgObjRes.options.data.content).toBe('Hello World');
462
+ const createMsgObjRes = await sdk.channel.message.create('acme-corp', 'chan_123', { content: 'Hello World' }) as any;
463
+ expect(createMsgObjRes.content).toBe('Hello World');
438
464
  });
439
465
 
440
466
  it('should support dm.message namespaces', async () => {
@@ -445,25 +471,23 @@ describe('ScrymeSDK', () => {
445
471
 
446
472
  // DM list
447
473
  const listDmsRes = await sdk.dm.list() as any;
448
- expect(listDmsRes.conversations).toBeDefined();
474
+ expect(listDmsRes).toBeDefined();
449
475
 
450
476
  // DM create
451
477
  const createDmRes = await sdk.dm.create({ userId: 'user_123' }) as any;
452
- expect(createDmRes.data.userId).toBe('user_123');
478
+ expect(createDmRes).toBeDefined();
453
479
 
454
480
  // DM message list
455
- const messagesRes = await sdk.dm.message.list('dm_123', { limit: 5 }) as any;
456
- expect(messagesRes.dmId).toBe('dm_123');
457
- expect(messagesRes.params.limit).toBe(5);
481
+ const messagesRes = await sdk.dm.message.list('dm_123', { limit: '5' }) as any;
482
+ expect(messagesRes.messages).toBeDefined();
458
483
 
459
484
  // DM message create (with string)
460
485
  const createMsgStrRes = await sdk.dm.message.create('dm_123', 'Hello DM') as any;
461
- expect(createMsgStrRes.dmId).toBe('dm_123');
462
- expect(createMsgStrRes.options.data.content).toBe('Hello DM');
486
+ expect(createMsgStrRes.content).toBe('Hello DM');
463
487
 
464
488
  // DM message create (with object)
465
489
  const createMsgObjRes = await sdk.dm.message.create('dm_123', { content: 'Hello DM Object' }) as any;
466
- expect(createMsgObjRes.options.data.content).toBe('Hello DM Object');
490
+ expect(createMsgObjRes.content).toBe('Hello DM Object');
467
491
  });
468
492
 
469
493
  it('should route message update/delete/reaction dynamically based on "dm-" prefix', async () => {
@@ -49,12 +49,115 @@ export const setGlobalToken = (token: string | null) => {
49
49
 
50
50
  export const getGlobalToken = () => globalToken;
51
51
 
52
+ /**
53
+ * Custom SDK Error class for meaningful, developer-friendly error messages and structured error details.
54
+ */
55
+ export class ScrymeSDKError extends Error {
56
+ public readonly status?: number;
57
+ public readonly code?: string;
58
+ public readonly data?: any;
59
+ public readonly rawError?: any;
60
+
61
+ constructor(
62
+ message: string,
63
+ options?: {
64
+ status?: number;
65
+ code?: string;
66
+ data?: any;
67
+ rawError?: any;
68
+ }
69
+ ) {
70
+ super(message);
71
+ this.name = 'ScrymeSDKError';
72
+ this.status = options?.status;
73
+ this.code = options?.code;
74
+ this.data = options?.data;
75
+ this.rawError = options?.rawError;
76
+
77
+ Object.setPrototypeOf(this, new.target.prototype);
78
+ }
79
+ }
80
+
81
+ export { ScrymeSDKError as ScrymeError };
82
+
83
+ /**
84
+ * Parses any error (AxiosError, Error, or unknown object) into a clean, meaningful ScrymeSDKError.
85
+ */
86
+ export function parseSDKError(error: any): ScrymeSDKError {
87
+ if (error instanceof ScrymeSDKError) {
88
+ return error;
89
+ }
90
+
91
+ if (axios.isAxiosError(error)) {
92
+ const response = error.response;
93
+ const status = response?.status;
94
+ const responseData = response?.data;
95
+ const code = error.code || (responseData && typeof responseData === 'object' ? responseData.code : undefined);
96
+
97
+ let message: string | undefined;
98
+
99
+ if (responseData) {
100
+ if (typeof responseData === 'string' && responseData.trim()) {
101
+ message = responseData;
102
+ } else if (typeof responseData === 'object' && responseData !== null) {
103
+ if (Array.isArray(responseData.message)) {
104
+ message = responseData.message.filter(Boolean).join('; ');
105
+ } else if (typeof responseData.message === 'string' && responseData.message.trim()) {
106
+ message = responseData.message;
107
+ } else if (typeof responseData.error === 'string' && responseData.error.trim()) {
108
+ message = responseData.error;
109
+ } else if (responseData.error && typeof responseData.error === 'object' && typeof responseData.error.message === 'string') {
110
+ message = responseData.error.message;
111
+ } else if (typeof responseData.detail === 'string' && responseData.detail.trim()) {
112
+ message = responseData.detail;
113
+ }
114
+ }
115
+ }
116
+
117
+ if (!message) {
118
+ if (response?.statusText) {
119
+ message = `HTTP ${status}: ${response.statusText}`;
120
+ } else if (error.message && !error.message.startsWith('Request failed with status code')) {
121
+ message = error.message;
122
+ } else if (status) {
123
+ message = `Request failed with status code ${status}`;
124
+ } else if (error.message) {
125
+ message = error.message;
126
+ } else {
127
+ message = 'An unexpected error occurred during API request';
128
+ }
129
+ }
130
+
131
+ return new ScrymeSDKError(message, {
132
+ status,
133
+ code,
134
+ data: responseData,
135
+ rawError: error,
136
+ });
137
+ }
138
+
139
+ if (error instanceof Error) {
140
+ return new ScrymeSDKError(error.message, {
141
+ rawError: error,
142
+ });
143
+ }
144
+
145
+ return new ScrymeSDKError(typeof error === 'string' ? error : 'An unexpected error occurred', {
146
+ rawError: error,
147
+ });
148
+ }
149
+
52
150
  export const AXIOS_INSTANCE = axios.create({
53
151
  baseURL: getBaseURL(),
54
152
  timeout: 10000,
55
153
  withCredentials: true,
56
154
  });
57
155
 
156
+ AXIOS_INSTANCE.interceptors.response.use(
157
+ response => response,
158
+ error => Promise.reject(parseSDKError(error))
159
+ );
160
+
58
161
  AXIOS_INSTANCE.interceptors.request.use(config => {
59
162
  if (!config.baseURL) {
60
163
  config.baseURL = getBaseURL();
@@ -144,7 +247,11 @@ export const customInstance = <T>(
144
247
  ...options,
145
248
  headers: mergedHeaders,
146
249
  cancelToken: source.token,
147
- }).then(({ data }) => data);
250
+ })
251
+ .then(({ data }) => data)
252
+ .catch(error => {
253
+ throw parseSDKError(error);
254
+ });
148
255
 
149
256
  // @ts-ignore
150
257
  promise.cancel = () => {
@@ -154,5 +261,5 @@ export const customInstance = <T>(
154
261
  return promise;
155
262
  };
156
263
 
157
- export type ErrorType<Error> = AxiosError<Error>;
264
+ export type ErrorType<Error> = ScrymeSDKError;
158
265
  export type BodyType<Body> = Body;