@unito/integration-sdk 0.1.7 → 0.1.9

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.
Files changed (39) hide show
  1. package/.eslintrc.cjs +2 -0
  2. package/dist/src/errors.d.ts +0 -6
  3. package/dist/src/errors.js +3 -6
  4. package/dist/src/handler.d.ts +3 -3
  5. package/dist/src/httpErrors.d.ts +3 -3
  6. package/dist/src/httpErrors.js +5 -5
  7. package/dist/src/index.cjs +829 -0
  8. package/dist/src/index.d.ts +1 -0
  9. package/dist/src/index.js +1 -0
  10. package/dist/src/integration.d.ts +0 -1
  11. package/dist/src/integration.js +2 -7
  12. package/dist/src/middlewares/errors.d.ts +9 -1
  13. package/dist/src/middlewares/errors.js +28 -13
  14. package/dist/src/middlewares/finish.d.ts +3 -1
  15. package/dist/src/middlewares/finish.js +25 -2
  16. package/dist/src/resources/cache.d.ts +18 -4
  17. package/dist/src/resources/cache.js +52 -21
  18. package/dist/src/resources/context.d.ts +1 -1
  19. package/dist/src/resources/logger.d.ts +49 -10
  20. package/dist/src/resources/logger.js +64 -18
  21. package/dist/test/errors.test.js +1 -0
  22. package/dist/test/middlewares/errors.test.js +1 -1
  23. package/dist/test/resources/cache.test.js +7 -15
  24. package/dist/test/resources/logger.test.js +53 -6
  25. package/package.json +11 -4
  26. package/src/errors.ts +2 -6
  27. package/src/handler.ts +4 -4
  28. package/src/httpErrors.ts +6 -6
  29. package/src/index.ts +1 -0
  30. package/src/integration.ts +2 -9
  31. package/src/middlewares/errors.ts +39 -14
  32. package/src/middlewares/finish.ts +28 -3
  33. package/src/resources/cache.ts +66 -23
  34. package/src/resources/context.ts +1 -1
  35. package/src/resources/logger.ts +84 -24
  36. package/test/errors.test.ts +1 -0
  37. package/test/middlewares/errors.test.ts +1 -1
  38. package/test/resources/cache.test.ts +7 -17
  39. package/test/resources/logger.test.ts +60 -7
@@ -1,4 +1,5 @@
1
1
  export * as Api from '@unito/integration-api';
2
+ export { Cache } from './resources/cache.js';
2
3
  export { default as Integration } from './integration.js';
3
4
  export * from './handler.js';
4
5
  export { Provider, type Response as ProviderResponse, type RequestOptions as ProviderRequestOptions, type RateLimiter, } from './resources/provider.js';
package/dist/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /* c8 ignore start */
2
2
  export * as Api from '@unito/integration-api';
3
+ export { Cache } from './resources/cache.js';
3
4
  export { default as Integration } from './integration.js';
4
5
  export * from './handler.js';
5
6
  export { Provider, } from './resources/provider.js';
@@ -5,7 +5,6 @@ type Options = {
5
5
  export default class Integration {
6
6
  private handlers;
7
7
  private instance;
8
- private cache;
9
8
  private port;
10
9
  constructor(options?: Options);
11
10
  addHandler(path: string, handlers: HandlersInput): void;
@@ -7,7 +7,7 @@ import selectsMiddleware from './middlewares/selects.js';
7
7
  import errorsMiddleware from './middlewares/errors.js';
8
8
  import finishMiddleware from './middlewares/finish.js';
9
9
  import notFoundMiddleware from './middlewares/notFound.js';
10
- import { initializeCache } from './resources/cache.js';
10
+ import { shutdownCaches } from './resources/cache.js';
11
11
  import { Handler } from './handler.js';
12
12
  function printErrorMessage(message) {
13
13
  console.error();
@@ -17,7 +17,6 @@ function printErrorMessage(message) {
17
17
  export default class Integration {
18
18
  handlers;
19
19
  instance = undefined;
20
- cache = undefined;
21
20
  port;
22
21
  constructor(options = {}) {
23
22
  this.port = options.port || 9200;
@@ -56,8 +55,6 @@ export default class Integration {
56
55
  }
57
56
  }
58
57
  start() {
59
- // Initialize the cache.
60
- this.cache = initializeCache();
61
58
  // Express Server initialization
62
59
  const app = express();
63
60
  // Parse query strings with https://github.com/ljharb/qs.
@@ -97,9 +94,7 @@ export default class Integration {
97
94
  if (this.instance) {
98
95
  this.instance.close();
99
96
  }
100
- if (this.cache) {
101
- await this.cache.quit();
102
- }
97
+ await shutdownCaches();
103
98
  }
104
99
  catch (e) {
105
100
  console.error('Failed to gracefully exit', e);
@@ -1,3 +1,11 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
- declare const middleware: (err: Error, _req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
2
+ import { Error as ApiError } from '@unito/integration-api';
3
+ declare global {
4
+ namespace Express {
5
+ interface Locals {
6
+ error?: ApiError;
7
+ }
8
+ }
9
+ }
10
+ declare const middleware: (err: Error, _req: Request, res: Response, next: NextFunction) => void;
3
11
  export default middleware;
@@ -3,20 +3,35 @@ const middleware = (err, _req, res, next) => {
3
3
  if (res.headersSent) {
4
4
  return next(err);
5
5
  }
6
- if (process.env.NODE_ENV !== 'production') {
7
- res.locals.logger.error(err);
8
- }
6
+ let error;
9
7
  if (err instanceof HttpError) {
10
- return res.status(err.status).json(err.message);
8
+ error = {
9
+ code: err.status.toString(),
10
+ message: err.message,
11
+ details: {
12
+ stack: err.stack,
13
+ },
14
+ };
15
+ }
16
+ else {
17
+ error = {
18
+ code: '500',
19
+ message: 'Oops! Something went wrong',
20
+ originalError: {
21
+ code: err.name,
22
+ message: err.message,
23
+ details: {
24
+ stack: err.stack,
25
+ },
26
+ },
27
+ };
28
+ }
29
+ res.locals.error = structuredClone(error);
30
+ // Keep the stack details in development for the Debugger
31
+ if (process.env.NODE_ENV !== 'development') {
32
+ delete error.details;
33
+ delete error.originalError?.details;
11
34
  }
12
- const originalError = {
13
- code: err.name,
14
- message: err.message,
15
- };
16
- res.status(500).json({
17
- code: '500',
18
- message: 'Oops! Something went wrong',
19
- originalError,
20
- });
35
+ res.status(Number(error.code)).json(error);
21
36
  };
22
37
  export default middleware;
@@ -1,9 +1,11 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
- import Logger from '../resources/logger.js';
2
+ import { Error as ApiError } from '@unito/integration-api';
3
+ import { default as Logger } from '../resources/logger.js';
3
4
  declare global {
4
5
  namespace Express {
5
6
  interface Locals {
6
7
  logger: Logger;
8
+ error?: ApiError;
7
9
  }
8
10
  }
9
11
  }
@@ -1,8 +1,31 @@
1
1
  const middleware = (req, res, next) => {
2
2
  if (req.originalUrl !== '/health') {
3
3
  res.on('finish', function () {
4
- const loggerLevel = res.statusCode >= 500 ? 'error' : 'info';
5
- res.locals.logger[loggerLevel](`${req.method} ${req.originalUrl} ${res.statusCode}`);
4
+ const error = res.locals.error;
5
+ const message = `${req.method} ${req.originalUrl} ${res.statusCode}`;
6
+ const metadata = {
7
+ // Use reserved and standard attributes of Datadog
8
+ // https://app.datadoghq.com/logs/pipelines/standard-attributes
9
+ http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
10
+ ...(error
11
+ ? {
12
+ error: {
13
+ kind: error.message,
14
+ stack: (error.originalError?.details?.stack ?? error.details?.stack),
15
+ message: error.originalError?.message ?? error.message,
16
+ },
17
+ }
18
+ : {}),
19
+ };
20
+ if ([404, 429].includes(res.statusCode)) {
21
+ res.locals.logger.warn(message, metadata);
22
+ }
23
+ else if (res.statusCode >= 400) {
24
+ res.locals.logger.error(message, metadata);
25
+ }
26
+ else {
27
+ res.locals.logger.info(message, metadata);
28
+ }
6
29
  });
7
30
  }
8
31
  next();
@@ -1,4 +1,18 @@
1
- import { CacheInstance } from 'cachette';
2
- export type Cache = CacheInstance;
3
- export declare function initializeCache(): Cache;
4
- export declare function generateCacheKey(value: string): string;
1
+ import { FetchingFunction, CachableValue } from 'cachette';
2
+ export declare const shutdownCaches: () => Promise<PromiseSettledResult<void>[]>;
3
+ export declare class Cache {
4
+ private cacheInstance;
5
+ private constructor();
6
+ getOrFetchValue<F extends FetchingFunction = FetchingFunction>(key: string, ttl: number, fetcher: F, lockTtl?: number, shouldCacheError?: (err: Error) => boolean): Promise<ReturnType<F>>;
7
+ getValue(key: string): Promise<CachableValue>;
8
+ setValue(key: string, value: CachableValue, ttl?: number): Promise<boolean>;
9
+ delValue(key: string): Promise<void>;
10
+ getTtl(key: string): Promise<number | undefined>;
11
+ /**
12
+ * Initializes a WriteThroughCache instance with the provided redis url if present, or a LocalCache otherwise.
13
+ *
14
+ * @param redisUrl - The redis url to connect to (optional).
15
+ * @returns A cache instance.
16
+ */
17
+ static create(redisUrl?: string): Cache;
18
+ }
@@ -1,25 +1,56 @@
1
1
  import { WriteThroughCache, LocalCache } from 'cachette';
2
- import { createHash } from 'node:crypto';
3
2
  import * as uuid from 'uuid';
4
3
  import Logger from './logger.js';
5
- export function initializeCache() {
6
- const cacheInstance = process.env.REDIS_URL ? new WriteThroughCache(process.env.REDIS_URL) : new LocalCache();
7
- // Intended: the correlation id will be the same for all logs of Cachette.
8
- const correlationId = uuid.v4();
9
- const logger = new Logger({ correlation_id: correlationId });
10
- cacheInstance
11
- .on('info', message => {
12
- logger.info(message);
13
- })
14
- .on('warn', message => {
15
- logger.warn(message);
16
- })
17
- .on('error', message => {
18
- logger.error(message);
19
- });
20
- return cacheInstance;
21
- }
22
- const shake256 = createHash('shake256');
23
- export function generateCacheKey(value) {
24
- return shake256.update(value).digest('hex');
4
+ /**
5
+ * Array of created caches kept to allow for graceful shutdown on exit signals.
6
+ */
7
+ const caches = [];
8
+ export const shutdownCaches = async () => {
9
+ return Promise.allSettled(caches.map(cache => cache.quit()));
10
+ };
11
+ export class Cache {
12
+ cacheInstance;
13
+ constructor(cacheInstance) {
14
+ this.cacheInstance = cacheInstance;
15
+ }
16
+ getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError) {
17
+ return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
18
+ }
19
+ getValue(key) {
20
+ return this.cacheInstance.getValue(key);
21
+ }
22
+ setValue(key, value, ttl) {
23
+ return this.cacheInstance.setValue(key, value, ttl);
24
+ }
25
+ delValue(key) {
26
+ return this.cacheInstance.delValue(key);
27
+ }
28
+ getTtl(key) {
29
+ return this.cacheInstance.getTtl(key);
30
+ }
31
+ /**
32
+ * Initializes a WriteThroughCache instance with the provided redis url if present, or a LocalCache otherwise.
33
+ *
34
+ * @param redisUrl - The redis url to connect to (optional).
35
+ * @returns A cache instance.
36
+ */
37
+ static create(redisUrl) {
38
+ const cacheInstance = redisUrl ? new WriteThroughCache(redisUrl) : new LocalCache();
39
+ // Push to the array of caches for graceful shutdown on exit signals.
40
+ caches.push(cacheInstance);
41
+ // Intended: the correlation id will be the same for all logs of Cachette.
42
+ const correlationId = uuid.v4();
43
+ const logger = new Logger({ correlation_id: correlationId });
44
+ cacheInstance
45
+ .on('info', message => {
46
+ logger.info(message);
47
+ })
48
+ .on('warn', message => {
49
+ logger.warn(message);
50
+ })
51
+ .on('error', message => {
52
+ logger.error(message);
53
+ });
54
+ return new Cache(cacheInstance);
55
+ }
25
56
  }
@@ -69,7 +69,7 @@ export type ParseWebhooksContext<P extends Record<string, string> = Record<strin
69
69
  export type UpdateWebhookSubscriptionsContext<P extends Record<string, string> = Record<string, never>, Q extends Record<string, string> = Record<string, never>, B extends API.WebhookSubscriptionRequestPayload = API.WebhookSubscriptionRequestPayload> = Context<P, Q> & {
70
70
  body: B;
71
71
  };
72
- export type AckknowledgeWebhooksContext<P extends Record<string, string> = Record<string, never>, Q extends Record<string, string> = Record<string, never>, B extends API.WebhookParseRequestPayload = API.WebhookParseRequestPayload> = Omit<Context<P, Q>, 'credentials'> & {
72
+ export type AcknowledgeWebhooksContext<P extends Record<string, string> = Record<string, never>, Q extends Record<string, string> = Record<string, never>, B extends API.WebhookParseRequestPayload = API.WebhookParseRequestPayload> = Omit<Context<P, Q>, 'credentials'> & {
73
73
  body: B;
74
74
  };
75
75
  interface ParsedQueryString {
@@ -1,14 +1,53 @@
1
+ type PrimitiveValue = undefined | null | string | string[] | number | number[] | boolean | boolean[];
2
+ type Value = {
3
+ [key: string]: PrimitiveValue | Value;
4
+ };
5
+ export type Metadata = Value & {
6
+ message?: never;
7
+ };
8
+ type ForbidenMetadataKey = 'message';
1
9
  export default class Logger {
2
10
  private metadata;
3
- constructor(metadata?: Record<string, unknown>);
4
- private send;
5
- log(data: unknown): void;
6
- error(data: unknown): void;
7
- warn(data: unknown): void;
8
- info(data: unknown): void;
9
- debug(data: unknown): void;
10
- decorate(metadata: Record<string, unknown>): void;
11
- getMetadata(): Record<string, unknown>;
12
- setMetadata(key: string, value: unknown): void;
11
+ constructor(metadata?: Metadata);
12
+ /**
13
+ * Logs a message with the 'log' log level.
14
+ * @param message The message to be logged.
15
+ * @param metadata Optional metadata to be associated with the log message.
16
+ */
17
+ log(message: string, metadata?: Metadata): void;
18
+ /**
19
+ * Logs an error message with the 'error' log level.
20
+ * @param message The error message to be logged.
21
+ * @param metadata Optional metadata to be associated with the log message.
22
+ */
23
+ error(message: string, metadata?: Metadata): void;
24
+ /**
25
+ * Logs a warning message with the 'warn' log level.
26
+ * @param message The warning message to be logged.
27
+ * @param metadata Optional metadata to be associated with the log message.
28
+ */
29
+ warn(message: string, metadata?: Metadata): void;
30
+ /**
31
+ * Logs an informational message with the 'info' log level.
32
+ * @param message The informational message to be logged.
33
+ * @param metadata Optional metadata to be associated with the log message.
34
+ */
35
+ info(message: string, metadata?: Metadata): void;
36
+ /**
37
+ * Logs a debug message with the 'debug' log level.
38
+ * @param message The debug message to be logged.
39
+ * @param metadata Optional metadata to be associated with the log message.
40
+ */
41
+ debug(message: string, metadata?: Metadata): void;
42
+ /**
43
+ * Decorates the logger with additional metadata.
44
+ * @param metadata Additional metadata to be added to the logger.
45
+ */
46
+ decorate(metadata: Metadata): void;
47
+ getMetadata(): Metadata;
48
+ setMetadata<Key extends string>(key: Key extends ForbidenMetadataKey ? never : Key, value: PrimitiveValue | Value): void;
13
49
  clearMetadata(): void;
50
+ private send;
51
+ private snakifyKeys;
14
52
  }
53
+ export {};
@@ -7,30 +7,54 @@ var LogLevel;
7
7
  LogLevel["DEBUG"] = "debug";
8
8
  })(LogLevel || (LogLevel = {}));
9
9
  export default class Logger {
10
- metadata = {};
11
- constructor(metadata) {
12
- if (metadata) {
13
- this.metadata = structuredClone(metadata);
14
- }
15
- }
16
- send(logLevel, message) {
17
- console[logLevel](JSON.stringify({ message, ...this.metadata }));
10
+ metadata;
11
+ constructor(metadata = {}) {
12
+ this.metadata = structuredClone(metadata);
18
13
  }
19
- log(data) {
20
- this.send(LogLevel.LOG, data);
14
+ /**
15
+ * Logs a message with the 'log' log level.
16
+ * @param message The message to be logged.
17
+ * @param metadata Optional metadata to be associated with the log message.
18
+ */
19
+ log(message, metadata) {
20
+ this.send(LogLevel.LOG, message, metadata);
21
21
  }
22
- error(data) {
23
- this.send(LogLevel.ERROR, data);
22
+ /**
23
+ * Logs an error message with the 'error' log level.
24
+ * @param message The error message to be logged.
25
+ * @param metadata Optional metadata to be associated with the log message.
26
+ */
27
+ error(message, metadata) {
28
+ this.send(LogLevel.ERROR, message, metadata);
24
29
  }
25
- warn(data) {
26
- this.send(LogLevel.WARN, data);
30
+ /**
31
+ * Logs a warning message with the 'warn' log level.
32
+ * @param message The warning message to be logged.
33
+ * @param metadata Optional metadata to be associated with the log message.
34
+ */
35
+ warn(message, metadata) {
36
+ this.send(LogLevel.WARN, message, metadata);
27
37
  }
28
- info(data) {
29
- this.send(LogLevel.INFO, data);
38
+ /**
39
+ * Logs an informational message with the 'info' log level.
40
+ * @param message The informational message to be logged.
41
+ * @param metadata Optional metadata to be associated with the log message.
42
+ */
43
+ info(message, metadata) {
44
+ this.send(LogLevel.INFO, message, metadata);
30
45
  }
31
- debug(data) {
32
- this.send(LogLevel.DEBUG, data);
46
+ /**
47
+ * Logs a debug message with the 'debug' log level.
48
+ * @param message The debug message to be logged.
49
+ * @param metadata Optional metadata to be associated with the log message.
50
+ */
51
+ debug(message, metadata) {
52
+ this.send(LogLevel.DEBUG, message, metadata);
33
53
  }
54
+ /**
55
+ * Decorates the logger with additional metadata.
56
+ * @param metadata Additional metadata to be added to the logger.
57
+ */
34
58
  decorate(metadata) {
35
59
  this.metadata = { ...this.metadata, ...metadata };
36
60
  }
@@ -43,4 +67,26 @@ export default class Logger {
43
67
  clearMetadata() {
44
68
  this.metadata = {};
45
69
  }
70
+ send(logLevel, message, metadata) {
71
+ const processedMessage = this.snakifyKeys({
72
+ ...this.metadata,
73
+ ...metadata,
74
+ message,
75
+ });
76
+ if (process.env.NODE_ENV === 'development') {
77
+ console[logLevel](JSON.stringify(processedMessage, null, 2));
78
+ }
79
+ else {
80
+ console[logLevel](JSON.stringify(processedMessage));
81
+ }
82
+ }
83
+ snakifyKeys(value) {
84
+ const result = {};
85
+ for (const key in value) {
86
+ const deepValue = typeof value[key] === 'object' ? this.snakifyKeys(value[key]) : value[key];
87
+ const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
88
+ result[snakifiedKey] = deepValue;
89
+ }
90
+ return result;
91
+ }
46
92
  }
@@ -8,6 +8,7 @@ describe('handleErrorResponse', () => {
8
8
  assert.ok(errors.buildHttpError(403, 'forbidden') instanceof httpErrors.UnauthorizedError);
9
9
  assert.ok(errors.buildHttpError(404, 'not found') instanceof httpErrors.NotFoundError);
10
10
  assert.ok(errors.buildHttpError(408, 'timeout') instanceof httpErrors.TimeoutError);
11
+ assert.ok(errors.buildHttpError(410, 'resource gone') instanceof httpErrors.ResourceGoneError);
11
12
  assert.ok(errors.buildHttpError(422, 'unprocessable entity') instanceof httpErrors.UnprocessableEntityError);
12
13
  assert.ok(errors.buildHttpError(429, 'rate limit exceeded') instanceof httpErrors.RateLimitExceededError);
13
14
  assert.ok(errors.buildHttpError(500, 'internal server error') instanceof httpErrors.HttpError);
@@ -39,7 +39,7 @@ describe('errors middleware', () => {
39
39
  };
40
40
  middleware(new HttpError('httpError', 429), {}, response, () => { });
41
41
  assert.strictEqual(actualStatus, 429);
42
- assert.strictEqual(actualJson, 'httpError');
42
+ assert.deepEqual(actualJson, { code: '429', message: 'httpError' });
43
43
  });
44
44
  it('handles other error', () => {
45
45
  let actualStatus;
@@ -1,25 +1,17 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
3
  import { LocalCache } from 'cachette';
4
- import { generateCacheKey, initializeCache } from '../../src/resources/cache.js';
4
+ import { Cache, shutdownCaches } from '../../src/resources/cache.js';
5
5
  describe('Cache', () => {
6
6
  describe('initializeCache', () => {
7
- it('no redis url returns LocalCache', () => {
8
- const cache = initializeCache();
9
- assert.equal(cache instanceof LocalCache, true);
10
- cache.quit();
7
+ it('no redis url returns Cache with a inner LocalCache', async () => {
8
+ const cache = Cache.create();
9
+ assert.ok(cache instanceof Cache);
10
+ assert.ok(cache['cacheInstance'] instanceof LocalCache);
11
+ await shutdownCaches();
11
12
  });
12
13
  it('redis url returns (tries) WriteThroughCache', () => {
13
- process.env.REDIS_URL = 'fakeredis'; //'redis://localhost:6379';
14
- assert.throws(() => initializeCache(), Error, 'Invalid redis url fakereis.');
15
- });
16
- });
17
- describe('generateCacheKey', () => {
18
- it('hashes string and returns hex value', () => {
19
- const value = 'test';
20
- const hash = generateCacheKey(value);
21
- assert.equal(typeof hash, 'string');
22
- assert.equal(hash.length, 64);
14
+ assert.throws(() => Cache.create('fakeredis'), Error, 'Invalid redis url fakereis.');
23
15
  });
24
16
  });
25
17
  });
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
- import Logger from '../../src/resources/logger.js';
3
+ import { default as Logger } from '../../src/resources/logger.js';
4
4
  describe('Logger', () => {
5
5
  it('metadata', () => {
6
6
  let metadata = { correlation_id: 'test' };
@@ -33,35 +33,82 @@ describe('Logger', () => {
33
33
  logger.log('test');
34
34
  assert.strictEqual(logSpy.mock.calls.length, 1);
35
35
  assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
36
- JSON.stringify({ message: 'test', correlation_id: '123456789' }),
36
+ JSON.stringify({ correlation_id: '123456789', message: 'test' }),
37
37
  ]);
38
38
  const errorSpy = testContext.mock.method(global.console, 'error', () => { });
39
39
  assert.strictEqual(errorSpy.mock.calls.length, 0);
40
40
  logger.error('test');
41
41
  assert.strictEqual(errorSpy.mock.calls.length, 1);
42
42
  assert.deepEqual(errorSpy.mock.calls[0]?.arguments, [
43
- JSON.stringify({ message: 'test', correlation_id: '123456789' }),
43
+ JSON.stringify({ correlation_id: '123456789', message: 'test' }),
44
44
  ]);
45
45
  const warnSpy = testContext.mock.method(global.console, 'warn', () => { });
46
46
  assert.strictEqual(warnSpy.mock.calls.length, 0);
47
47
  logger.warn('test');
48
48
  assert.strictEqual(warnSpy.mock.calls.length, 1);
49
49
  assert.deepEqual(warnSpy.mock.calls[0]?.arguments, [
50
- JSON.stringify({ message: 'test', correlation_id: '123456789' }),
50
+ JSON.stringify({ correlation_id: '123456789', message: 'test' }),
51
51
  ]);
52
52
  const infoSpy = testContext.mock.method(global.console, 'info', () => { });
53
53
  assert.strictEqual(infoSpy.mock.calls.length, 0);
54
54
  logger.info('test');
55
55
  assert.strictEqual(infoSpy.mock.calls.length, 1);
56
56
  assert.deepEqual(infoSpy.mock.calls[0]?.arguments, [
57
- JSON.stringify({ message: 'test', correlation_id: '123456789' }),
57
+ JSON.stringify({ correlation_id: '123456789', message: 'test' }),
58
58
  ]);
59
59
  const debugSpy = testContext.mock.method(global.console, 'debug', () => { });
60
60
  assert.strictEqual(debugSpy.mock.calls.length, 0);
61
61
  logger.debug('test');
62
62
  assert.strictEqual(debugSpy.mock.calls.length, 1);
63
63
  assert.deepEqual(debugSpy.mock.calls[0]?.arguments, [
64
- JSON.stringify({ message: 'test', correlation_id: '123456789' }),
64
+ JSON.stringify({ correlation_id: '123456789', message: 'test' }),
65
+ ]);
66
+ });
67
+ it('merges message payload with metadata', testContext => {
68
+ const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
69
+ const logger = new Logger(metadata);
70
+ const logSpy = testContext.mock.method(global.console, 'log', () => { });
71
+ assert.strictEqual(logSpy.mock.calls.length, 0);
72
+ logger.log('test', { error: { code: '200', message: 'Page Not Found' } });
73
+ assert.strictEqual(logSpy.mock.calls.length, 1);
74
+ assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
75
+ JSON.stringify({
76
+ correlation_id: '123456789',
77
+ http: { method: 'GET' },
78
+ error: { code: '200', message: 'Page Not Found' },
79
+ message: 'test',
80
+ }),
81
+ ]);
82
+ });
83
+ it('overwrites conflicting metadata keys (1st level) with message payload', testContext => {
84
+ const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
85
+ const logger = new Logger(metadata);
86
+ const logSpy = testContext.mock.method(global.console, 'log', () => { });
87
+ assert.strictEqual(logSpy.mock.calls.length, 0);
88
+ logger.log('test', { http: { status_code: 200 } });
89
+ assert.strictEqual(logSpy.mock.calls.length, 1);
90
+ assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
91
+ JSON.stringify({
92
+ correlation_id: '123456789',
93
+ http: { status_code: 200 },
94
+ message: 'test',
95
+ }),
96
+ ]);
97
+ });
98
+ it('snakify keys of Message and Metadata', testContext => {
99
+ const metadata = { correlationId: '123456789', http: { method: 'GET', statusCode: 200 } };
100
+ const logger = new Logger(metadata);
101
+ const logSpy = testContext.mock.method(global.console, 'log', () => { });
102
+ assert.strictEqual(logSpy.mock.calls.length, 0);
103
+ logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
104
+ assert.strictEqual(logSpy.mock.calls.length, 1);
105
+ assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
106
+ JSON.stringify({
107
+ correlation_id: '123456789',
108
+ http: { method: 'GET', status_code: 200 },
109
+ error_context: { error_code: 200, error_message: 'Page Not Found' },
110
+ message: 'test',
111
+ }),
65
112
  ]);
66
113
  });
67
114
  });
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
7
7
  "exports": {
8
8
  "./package.json": "./package.json",
9
- ".": "./dist/src/index.js"
9
+ ".": {
10
+ "import": "./dist/src/index.js",
11
+ "require": "./dist/src/index.cjs"
12
+ }
10
13
  },
11
14
  "license": "LicenseRef-LICENSE",
12
15
  "engines": {
@@ -21,8 +24,10 @@
21
24
  "prepublishOnly": "npm run lint && npm run test",
22
25
  "prepare": "npm run compile",
23
26
  "lint": "eslint --fix src test --ext .ts && prettier --write src test",
24
- "compile": "tsc --build",
25
- "compile:watch": "npm run compile -- --watch",
27
+ "compile": "npm run compile:esm && npm run compile:cjs",
28
+ "compile:watch": "nodemon --watch \"src/**\" --ext js,ts --exec \"npm run compile\"",
29
+ "compile:esm": "tsc --build",
30
+ "compile:cjs": "rollup dist/src/index.js --file dist/src/index.cjs --format cjs",
26
31
  "test": "NODE_ENV=test node --test --no-warnings --loader ts-node/esm --test-name-pattern=${ONLY:-.*} $(find test -type f -name '*.test.ts')",
27
32
  "test:debug": "NODE_ENV=test node --test --loader ts-node/esm --test-name-pattern=${ONLY:-.*} --inspect-brk $(find test -type f -name '*.test.ts')",
28
33
  "ci:test": "npm run test"
@@ -36,7 +41,9 @@
36
41
  "c8": "9.x",
37
42
  "eslint": "8.x",
38
43
  "prettier": "3.x",
44
+ "rollup": "4.x",
39
45
  "ts-node": "10.x",
46
+ "nodemon": "3.x",
40
47
  "typescript": "5.x"
41
48
  },
42
49
  "dependencies": {