@unito/integration-sdk 2.3.4 → 2.3.6

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.
@@ -165,9 +165,10 @@ class Logger {
165
165
  const coloredMessage = Logger.colorize(message, processedLogs, logLevel);
166
166
  const metadata = {
167
167
  date: new Date(processedLogs.date).toISOString(),
168
- ...processedMetadata,
168
+ ...(processedMetadata.error && { error: processedMetadata.error }),
169
169
  };
170
- console[logLevel](`${coloredMessage} ${JSON.stringify(metadata, null, 2)}`);
170
+ const metadataString = Object.keys(metadata).length > 1 ? ` ${JSON.stringify(metadata, null, 2)}` : ` ${JSON.stringify(metadata)}`;
171
+ console[logLevel](`${coloredMessage}${metadataString}`);
171
172
  }
172
173
  else {
173
174
  console[logLevel](JSON.stringify(processedLogs));
@@ -219,9 +220,8 @@ class Logger {
219
220
  */
220
221
  static colorize(message, metadata, logLevel) {
221
222
  if (!process.stdout.isTTY) {
222
- return message;
223
+ return `${logLevel}: ${message}`;
223
224
  }
224
- const logOutput = `${logLevel}: ${message}`;
225
225
  // Extract status code from logs
226
226
  let statusCode;
227
227
  if (metadata.http && typeof metadata.http === 'object' && !Array.isArray(metadata.http)) {
@@ -236,28 +236,28 @@ class Logger {
236
236
  // Color based on status code first
237
237
  if (statusCode) {
238
238
  if (statusCode >= 400) {
239
- return util.styleText('red', logOutput);
239
+ return `${util.styleText('red', logLevel)}: ${util.styleText('red', message)}`;
240
240
  }
241
241
  else if (statusCode >= 300) {
242
- return util.styleText('yellow', logOutput);
242
+ return `${util.styleText('yellow', logLevel)}: ${util.styleText('yellow', message)}`;
243
243
  }
244
244
  else if (statusCode >= 200) {
245
- return util.styleText('green', logOutput);
245
+ return `${util.styleText('green', logLevel)}: ${util.styleText('green', message)}`;
246
246
  }
247
247
  }
248
248
  // Fall back to log level if no status code found
249
249
  switch (logLevel) {
250
250
  case LogLevel.ERROR:
251
- return util.styleText('red', logOutput);
251
+ return `${util.styleText('red', logLevel)}: ${util.styleText('red', message)}`;
252
252
  case LogLevel.WARN:
253
- return util.styleText('yellow', logOutput);
253
+ return `${util.styleText('yellow', logLevel)}: ${util.styleText('yellow', message)}`;
254
254
  case LogLevel.INFO:
255
255
  case LogLevel.LOG:
256
- return util.styleText('green', logOutput);
256
+ return `${util.styleText('green', logLevel)}: ${util.styleText('green', message)}`;
257
257
  case LogLevel.DEBUG:
258
- return util.styleText('cyan', logOutput);
258
+ return `${util.styleText('cyan', logLevel)}: ${util.styleText('cyan', message)}`;
259
259
  default:
260
- return logOutput;
260
+ return `${logLevel}: ${message}`;
261
261
  }
262
262
  }
263
263
  }
@@ -1440,13 +1440,18 @@ class Provider {
1440
1440
  * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
1441
1441
  * @returns The {@link Response} extracted from the provider.
1442
1442
  */
1443
- async delete(endpoint, options) {
1444
- return this.fetchWrapper(endpoint, null, {
1443
+ async delete(endpoint, options, body = null) {
1444
+ const defaultHeaders = {
1445
+ Accept: 'application/json',
1446
+ };
1447
+ // Only add Content-Type header when body is provided
1448
+ if (body !== null) {
1449
+ defaultHeaders['Content-Type'] = 'application/json';
1450
+ }
1451
+ return this.fetchWrapper(endpoint, body, {
1445
1452
  ...options,
1446
1453
  method: 'DELETE',
1447
- defaultHeaders: {
1448
- Accept: 'application/json',
1449
- },
1454
+ defaultHeaders,
1450
1455
  });
1451
1456
  }
1452
1457
  generateAbsoluteUrl(providerUrl, endpoint, queryParams) {
@@ -137,9 +137,10 @@ export default class Logger {
137
137
  const coloredMessage = Logger.colorize(message, processedLogs, logLevel);
138
138
  const metadata = {
139
139
  date: new Date(processedLogs.date).toISOString(),
140
- ...processedMetadata,
140
+ ...(processedMetadata.error && { error: processedMetadata.error }),
141
141
  };
142
- console[logLevel](`${coloredMessage} ${JSON.stringify(metadata, null, 2)}`);
142
+ const metadataString = Object.keys(metadata).length > 1 ? ` ${JSON.stringify(metadata, null, 2)}` : ` ${JSON.stringify(metadata)}`;
143
+ console[logLevel](`${coloredMessage}${metadataString}`);
143
144
  }
144
145
  else {
145
146
  console[logLevel](JSON.stringify(processedLogs));
@@ -191,9 +192,8 @@ export default class Logger {
191
192
  */
192
193
  static colorize(message, metadata, logLevel) {
193
194
  if (!process.stdout.isTTY) {
194
- return message;
195
+ return `${logLevel}: ${message}`;
195
196
  }
196
- const logOutput = `${logLevel}: ${message}`;
197
197
  // Extract status code from logs
198
198
  let statusCode;
199
199
  if (metadata.http && typeof metadata.http === 'object' && !Array.isArray(metadata.http)) {
@@ -208,28 +208,28 @@ export default class Logger {
208
208
  // Color based on status code first
209
209
  if (statusCode) {
210
210
  if (statusCode >= 400) {
211
- return styleText('red', logOutput);
211
+ return `${styleText('red', logLevel)}: ${styleText('red', message)}`;
212
212
  }
213
213
  else if (statusCode >= 300) {
214
- return styleText('yellow', logOutput);
214
+ return `${styleText('yellow', logLevel)}: ${styleText('yellow', message)}`;
215
215
  }
216
216
  else if (statusCode >= 200) {
217
- return styleText('green', logOutput);
217
+ return `${styleText('green', logLevel)}: ${styleText('green', message)}`;
218
218
  }
219
219
  }
220
220
  // Fall back to log level if no status code found
221
221
  switch (logLevel) {
222
222
  case LogLevel.ERROR:
223
- return styleText('red', logOutput);
223
+ return `${styleText('red', logLevel)}: ${styleText('red', message)}`;
224
224
  case LogLevel.WARN:
225
- return styleText('yellow', logOutput);
225
+ return `${styleText('yellow', logLevel)}: ${styleText('yellow', message)}`;
226
226
  case LogLevel.INFO:
227
227
  case LogLevel.LOG:
228
- return styleText('green', logOutput);
228
+ return `${styleText('green', logLevel)}: ${styleText('green', message)}`;
229
229
  case LogLevel.DEBUG:
230
- return styleText('cyan', logOutput);
230
+ return `${styleText('cyan', logLevel)}: ${styleText('cyan', message)}`;
231
231
  default:
232
- return logOutput;
232
+ return `${logLevel}: ${message}`;
233
233
  }
234
234
  }
235
235
  }
@@ -207,7 +207,7 @@ export declare class Provider {
207
207
  * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
208
208
  * @returns The {@link Response} extracted from the provider.
209
209
  */
210
- delete<T = undefined>(endpoint: string, options: RequestOptions): Promise<Response<T>>;
210
+ delete<T = undefined>(endpoint: string, options: RequestOptions, body?: RequestBody | null): Promise<Response<T>>;
211
211
  private generateAbsoluteUrl;
212
212
  private fetchWrapper;
213
213
  private handleError;
@@ -241,13 +241,18 @@ export class Provider {
241
241
  * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
242
242
  * @returns The {@link Response} extracted from the provider.
243
243
  */
244
- async delete(endpoint, options) {
245
- return this.fetchWrapper(endpoint, null, {
244
+ async delete(endpoint, options, body = null) {
245
+ const defaultHeaders = {
246
+ Accept: 'application/json',
247
+ };
248
+ // Only add Content-Type header when body is provided
249
+ if (body !== null) {
250
+ defaultHeaders['Content-Type'] = 'application/json';
251
+ }
252
+ return this.fetchWrapper(endpoint, body, {
246
253
  ...options,
247
254
  method: 'DELETE',
248
- defaultHeaders: {
249
- Accept: 'application/json',
250
- },
255
+ defaultHeaders,
251
256
  });
252
257
  }
253
258
  generateAbsoluteUrl(providerUrl, endpoint, queryParams) {
@@ -250,4 +250,61 @@ describe('Logger', () => {
250
250
  });
251
251
  }
252
252
  });
253
+ it('only logs date for metadata of successful requests in development', testContext => {
254
+ const originalEnv = process.env.NODE_ENV;
255
+ try {
256
+ process.env.NODE_ENV = 'development';
257
+ const infoSpy = testContext.mock.method(global.console, 'info', () => { });
258
+ const metadata = {
259
+ correlation_id: '123456789',
260
+ http: { method: 'GET', status_code: 200 },
261
+ user_id: 'user123',
262
+ request_id: 'req456',
263
+ };
264
+ const logger = new Logger(metadata);
265
+ logger.info('Test message without error');
266
+ assert.strictEqual(infoSpy.mock.calls.length, 1);
267
+ const loggedOutput = infoSpy.mock.calls[0]?.arguments[0];
268
+ assert.ok(loggedOutput.includes('Test message without error'));
269
+ // Should only contain date in metadata JSON
270
+ const metadataMatch = loggedOutput.match(/({.*})/);
271
+ assert.ok(metadataMatch);
272
+ const parsedMetadata = JSON.parse(metadataMatch[1]);
273
+ assert.ok(parsedMetadata.date);
274
+ assert.strictEqual(Object.keys(parsedMetadata).length, 1); // What matters: Only the date
275
+ }
276
+ finally {
277
+ process.env.NODE_ENV = originalEnv;
278
+ }
279
+ });
280
+ it('logs date & error stack for metadata of failed requests in development', testContext => {
281
+ const originalEnv = process.env.NODE_ENV;
282
+ try {
283
+ process.env.NODE_ENV = 'development';
284
+ const metadata = {
285
+ correlation_id: '123456789',
286
+ http: { method: 'GET', status_code: 200 },
287
+ user_id: 'user123',
288
+ request_id: 'req456',
289
+ };
290
+ const logger = new Logger(metadata);
291
+ // Test with error metadata
292
+ const errorSpy = testContext.mock.method(global.console, 'error', () => { });
293
+ logger.error('Test message with error', { error: { code: 500, message: 'Internal Server Error' } });
294
+ assert.strictEqual(errorSpy.mock.calls.length, 1);
295
+ const errorLoggedOutput = errorSpy.mock.calls[0]?.arguments[0];
296
+ assert.ok(errorLoggedOutput.includes('Test message with error'));
297
+ // Should contain both date and error in metadata JSON
298
+ const errorMetadataMatch = errorLoggedOutput.match(/({.*})/s);
299
+ assert.ok(errorMetadataMatch);
300
+ const parsedErrorMetadata = JSON.parse(errorMetadataMatch[1]);
301
+ assert.ok(parsedErrorMetadata.date);
302
+ assert.ok(parsedErrorMetadata.error);
303
+ assert.deepEqual(parsedErrorMetadata.error, { code: 500, message: 'Internal Server Error' });
304
+ assert.strictEqual(Object.keys(parsedErrorMetadata).length, 2); // What matters: Date and error only
305
+ }
306
+ finally {
307
+ process.env.NODE_ENV = originalEnv;
308
+ }
309
+ });
253
310
  });
@@ -404,6 +404,37 @@ describe('Provider', () => {
404
404
  ]);
405
405
  assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
406
406
  });
407
+ it('deleteWithBody', async (context) => {
408
+ const response = new Response('{"success": true}', {
409
+ status: 200,
410
+ headers: { 'Content-Type': 'application/json' },
411
+ });
412
+ const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
413
+ const requestBody = { webhookIds: [1, 2, 3] };
414
+ const actualResponse = await provider.delete('/webhook', {
415
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
416
+ logger: logger,
417
+ signal: new AbortController().signal,
418
+ additionnalheaders: { 'X-Additional-Header': 'value1' },
419
+ }, requestBody);
420
+ assert.equal(fetchMock.mock.calls.length, 1);
421
+ assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
422
+ 'www.myApi.com/webhook',
423
+ {
424
+ method: 'DELETE',
425
+ body: JSON.stringify(requestBody),
426
+ signal: new AbortController().signal,
427
+ headers: {
428
+ 'Content-Type': 'application/json',
429
+ Accept: 'application/json',
430
+ 'X-Custom-Provider-Header': 'value',
431
+ 'X-Provider-Credential-Header': 'apikey#1111',
432
+ 'X-Additional-Header': 'value1',
433
+ },
434
+ },
435
+ ]);
436
+ assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { success: true } });
437
+ });
407
438
  it('uses rate limiter if provided', async (context) => {
408
439
  const mockRateLimiter = context.mock.fn((_context, request) => Promise.resolve(request()));
409
440
  const rateLimitedProvider = new Provider({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "2.3.4",
3
+ "version": "2.3.6",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
@@ -168,10 +168,13 @@ export default class Logger {
168
168
 
169
169
  const metadata = {
170
170
  date: new Date(processedLogs.date).toISOString(),
171
- ...processedMetadata,
171
+ ...(processedMetadata.error && { error: processedMetadata.error }),
172
172
  };
173
173
 
174
- console[logLevel](`${coloredMessage} ${JSON.stringify(metadata, null, 2)}`);
174
+ const metadataString =
175
+ Object.keys(metadata).length > 1 ? ` ${JSON.stringify(metadata, null, 2)}` : ` ${JSON.stringify(metadata)}`;
176
+
177
+ console[logLevel](`${coloredMessage}${metadataString}`);
175
178
  } else {
176
179
  console[logLevel](JSON.stringify(processedLogs));
177
180
  }
@@ -228,11 +231,9 @@ export default class Logger {
228
231
  */
229
232
  private static colorize(message: string, metadata: Value, logLevel: LogLevel): string {
230
233
  if (!process.stdout.isTTY) {
231
- return message;
234
+ return `${logLevel}: ${message}`;
232
235
  }
233
236
 
234
- const logOutput = `${logLevel}: ${message}`;
235
-
236
237
  // Extract status code from logs
237
238
  let statusCode: number | undefined;
238
239
  if (metadata.http && typeof metadata.http === 'object' && !Array.isArray(metadata.http)) {
@@ -248,27 +249,27 @@ export default class Logger {
248
249
  // Color based on status code first
249
250
  if (statusCode) {
250
251
  if (statusCode >= 400) {
251
- return styleText('red', logOutput);
252
+ return `${styleText('red', logLevel)}: ${styleText('red', message)}`;
252
253
  } else if (statusCode >= 300) {
253
- return styleText('yellow', logOutput);
254
+ return `${styleText('yellow', logLevel)}: ${styleText('yellow', message)}`;
254
255
  } else if (statusCode >= 200) {
255
- return styleText('green', logOutput);
256
+ return `${styleText('green', logLevel)}: ${styleText('green', message)}`;
256
257
  }
257
258
  }
258
259
 
259
260
  // Fall back to log level if no status code found
260
261
  switch (logLevel) {
261
262
  case LogLevel.ERROR:
262
- return styleText('red', logOutput);
263
+ return `${styleText('red', logLevel)}: ${styleText('red', message)}`;
263
264
  case LogLevel.WARN:
264
- return styleText('yellow', logOutput);
265
+ return `${styleText('yellow', logLevel)}: ${styleText('yellow', message)}`;
265
266
  case LogLevel.INFO:
266
267
  case LogLevel.LOG:
267
- return styleText('green', logOutput);
268
+ return `${styleText('green', logLevel)}: ${styleText('green', message)}`;
268
269
  case LogLevel.DEBUG:
269
- return styleText('cyan', logOutput);
270
+ return `${styleText('cyan', logLevel)}: ${styleText('cyan', message)}`;
270
271
  default:
271
- return logOutput;
272
+ return `${logLevel}: ${message}`;
272
273
  }
273
274
  }
274
275
  }
@@ -336,13 +336,24 @@ export class Provider {
336
336
  * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
337
337
  * @returns The {@link Response} extracted from the provider.
338
338
  */
339
- public async delete<T = undefined>(endpoint: string, options: RequestOptions): Promise<Response<T>> {
340
- return this.fetchWrapper<T>(endpoint, null, {
339
+ public async delete<T = undefined>(
340
+ endpoint: string,
341
+ options: RequestOptions,
342
+ body: RequestBody | null = null,
343
+ ): Promise<Response<T>> {
344
+ const defaultHeaders: { Accept: string; 'Content-Type'?: string } = {
345
+ Accept: 'application/json',
346
+ };
347
+
348
+ // Only add Content-Type header when body is provided
349
+ if (body !== null) {
350
+ defaultHeaders['Content-Type'] = 'application/json';
351
+ }
352
+
353
+ return this.fetchWrapper<T>(endpoint, body, {
341
354
  ...options,
342
355
  method: 'DELETE',
343
- defaultHeaders: {
344
- Accept: 'application/json',
345
- },
356
+ defaultHeaders,
346
357
  });
347
358
  }
348
359
 
@@ -291,4 +291,73 @@ describe('Logger', () => {
291
291
  });
292
292
  }
293
293
  });
294
+
295
+ it('only logs date for metadata of successful requests in development', testContext => {
296
+ const originalEnv = process.env.NODE_ENV;
297
+
298
+ try {
299
+ process.env.NODE_ENV = 'development';
300
+
301
+ const infoSpy = testContext.mock.method(global.console, 'info', () => {});
302
+ const metadata = {
303
+ correlation_id: '123456789',
304
+ http: { method: 'GET', status_code: 200 },
305
+ user_id: 'user123',
306
+ request_id: 'req456',
307
+ };
308
+
309
+ const logger = new Logger(metadata);
310
+
311
+ logger.info('Test message without error');
312
+ assert.strictEqual(infoSpy.mock.calls.length, 1);
313
+
314
+ const loggedOutput = infoSpy.mock.calls[0]?.arguments[0];
315
+ assert.ok(loggedOutput.includes('Test message without error'));
316
+
317
+ // Should only contain date in metadata JSON
318
+ const metadataMatch = loggedOutput.match(/({.*})/);
319
+ assert.ok(metadataMatch);
320
+ const parsedMetadata = JSON.parse(metadataMatch[1]);
321
+ assert.ok(parsedMetadata.date);
322
+ assert.strictEqual(Object.keys(parsedMetadata).length, 1); // What matters: Only the date
323
+ } finally {
324
+ process.env.NODE_ENV = originalEnv;
325
+ }
326
+ });
327
+
328
+ it('logs date & error stack for metadata of failed requests in development', testContext => {
329
+ const originalEnv = process.env.NODE_ENV;
330
+
331
+ try {
332
+ process.env.NODE_ENV = 'development';
333
+
334
+ const metadata = {
335
+ correlation_id: '123456789',
336
+ http: { method: 'GET', status_code: 200 },
337
+ user_id: 'user123',
338
+ request_id: 'req456',
339
+ };
340
+
341
+ const logger = new Logger(metadata);
342
+
343
+ // Test with error metadata
344
+ const errorSpy = testContext.mock.method(global.console, 'error', () => {});
345
+ logger.error('Test message with error', { error: { code: 500, message: 'Internal Server Error' } });
346
+ assert.strictEqual(errorSpy.mock.calls.length, 1);
347
+
348
+ const errorLoggedOutput = errorSpy.mock.calls[0]?.arguments[0];
349
+ assert.ok(errorLoggedOutput.includes('Test message with error'));
350
+
351
+ // Should contain both date and error in metadata JSON
352
+ const errorMetadataMatch = errorLoggedOutput.match(/({.*})/s);
353
+ assert.ok(errorMetadataMatch);
354
+ const parsedErrorMetadata = JSON.parse(errorMetadataMatch[1]);
355
+ assert.ok(parsedErrorMetadata.date);
356
+ assert.ok(parsedErrorMetadata.error);
357
+ assert.deepEqual(parsedErrorMetadata.error, { code: 500, message: 'Internal Server Error' });
358
+ assert.strictEqual(Object.keys(parsedErrorMetadata).length, 2); // What matters: Date and error only
359
+ } finally {
360
+ process.env.NODE_ENV = originalEnv;
361
+ }
362
+ });
294
363
  });
@@ -486,6 +486,45 @@ describe('Provider', () => {
486
486
  assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
487
487
  });
488
488
 
489
+ it('deleteWithBody', async context => {
490
+ const response = new Response('{"success": true}', {
491
+ status: 200,
492
+ headers: { 'Content-Type': 'application/json' },
493
+ });
494
+
495
+ const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
496
+
497
+ const requestBody = { webhookIds: [1, 2, 3] };
498
+ const actualResponse = await provider.delete(
499
+ '/webhook',
500
+ {
501
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
502
+ logger: logger,
503
+ signal: new AbortController().signal,
504
+ additionnalheaders: { 'X-Additional-Header': 'value1' },
505
+ },
506
+ requestBody,
507
+ );
508
+
509
+ assert.equal(fetchMock.mock.calls.length, 1);
510
+ assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
511
+ 'www.myApi.com/webhook',
512
+ {
513
+ method: 'DELETE',
514
+ body: JSON.stringify(requestBody),
515
+ signal: new AbortController().signal,
516
+ headers: {
517
+ 'Content-Type': 'application/json',
518
+ Accept: 'application/json',
519
+ 'X-Custom-Provider-Header': 'value',
520
+ 'X-Provider-Credential-Header': 'apikey#1111',
521
+ 'X-Additional-Header': 'value1',
522
+ },
523
+ },
524
+ ]);
525
+ assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { success: true } });
526
+ });
527
+
489
528
  it('uses rate limiter if provided', async context => {
490
529
  const mockRateLimiter = context.mock.fn((_context, request) => Promise.resolve(request()));
491
530