core-services-sdk 1.2.1 → 1.2.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -0,0 +1,7 @@
1
+ import httpStatus from 'http-status'
2
+
3
+ export const GENERAL_ERROR = {
4
+ httpStatusCode: httpStatus.INTERNAL_SERVER_ERROR,
5
+ httpStatusText: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
6
+ code: `GENERAL.${httpStatus[httpStatus.INTERNAL_SERVER_ERROR]}`,
7
+ }
@@ -0,0 +1,90 @@
1
+ import httpStatus from 'http-status'
2
+
3
+ import { HttpError } from '../../http/HttpError.js'
4
+ import { GENERAL_ERROR } from '../error-codes.js'
5
+
6
+ /**
7
+ * Generic error-handling wrapper that logs and throws a safe error.
8
+ * @param {object} log - Logger with an .error method.
9
+ * @param {HttpError} defaultError - Error to throw if original error is not an HttpError.
10
+ * @returns {(funcToInvoke: () => Promise<any>) => Promise<any>}
11
+ */
12
+ export const withErrorHandling =
13
+ (log, defaultError) => async (funcToInvoke) => {
14
+ try {
15
+ return await funcToInvoke()
16
+ } catch (error) {
17
+ log.error(`[withErrorHandling] - Error: ${error?.stack || error}`)
18
+
19
+ if (!error) {
20
+ throw defaultError
21
+ }
22
+
23
+ if (HttpError.isInstanceOf(error)) {
24
+ throw error
25
+ }
26
+
27
+ const httpError = HttpError.FromError(error)
28
+
29
+ throw httpError
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Executes function with error handling and replies on failure.
35
+ *
36
+ * @param {object} params
37
+ * @param {Reply} params.reply - Fastify reply object
38
+ * @param {Logger} params.log - Logger object
39
+ * @param {HttpError} [params.defaultError] - Fallback error
40
+ */
41
+ export const withErrorHandlingReply =
42
+ ({ reply, log, defaultError = new HttpError(GENERAL_ERROR) }) =>
43
+ async (funcToInvoke) => {
44
+ try {
45
+ return await withErrorHandling(log, defaultError)(funcToInvoke)
46
+ } catch (error) {
47
+ const { code, httpStatusText, httpStatusCode } = error
48
+ reply.status(httpStatusCode).send({ code, httpStatusText })
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Executes function with error handling and replies on failure.
54
+ *
55
+ * @param {object} params
56
+ * @param {Reply} params.reply - Fastify reply object
57
+ * @param {Logger} params.log - Logger object
58
+ * @param {HttpError} [params.defaultError] - Fallback error
59
+ */
60
+ export const replyOnErrorOnly =
61
+ ({ reply, log, defaultError = new HttpError(GENERAL_ERROR) }) =>
62
+ async (funcToInvoke) => {
63
+ try {
64
+ return await withErrorHandling(log, defaultError)(funcToInvoke)
65
+ } catch (error) {
66
+ log.error(`[replyOnErrorOnly] - Error: ${error?.stack || error}`)
67
+ const isHttp = HttpError.isInstanceOf(error)
68
+ const errorMerged = isHttp
69
+ ? error
70
+ : new HttpError({
71
+ ...defaultError,
72
+ message: error?.message || defaultError.message,
73
+ code: error?.code || defaultError.code,
74
+ })
75
+
76
+ if (!isHttp && error?.stack) {
77
+ errorMerged.stack = error.stack
78
+ }
79
+
80
+ const exposed =
81
+ errorMerged.message ?? errorMerged.code ?? GENERAL_ERROR.httpStatusText
82
+
83
+ const status =
84
+ errorMerged.httpStatusCode && errorMerged.httpStatusCode in httpStatus
85
+ ? errorMerged.httpStatusCode
86
+ : httpStatus.INTERNAL_SERVER_ERROR
87
+
88
+ reply.status(status).send({ error: exposed })
89
+ }
90
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @typedef {object} Reply
3
+ * @property {(code: number) => Reply} status - Sets the HTTP status code
4
+ * @property {(payload: any) => void} send - Sends the response
5
+ */
6
+
7
+ /**
8
+ * @typedef {object} Logger
9
+ * @property {(message: string, ...args: any[]) => void} error - Logs an error message
10
+ */
@@ -0,0 +1,2 @@
1
+ export { GENERAL_ERROR } from './error-codes.js'
2
+ export * from './error-handlers/with-error-handling.js'
@@ -1,47 +1,74 @@
1
- import * as httpStatus from 'http-status'
1
+ import httpStatus from 'http-status'
2
2
 
3
- const DEFAULT_ERROR = {
4
- httpStatusCode: httpStatus.INTERNAL_SERVER_ERROR,
5
- code: 'UNKNOWN',
6
- httpStatusText: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
7
- }
8
-
9
- /**
10
- *
11
- */
12
3
  export class HttpError extends Error {
13
- /** @type {number} */
14
- httpStatusCode
15
- /** @type {string} */
4
+ /** @type {string|undefined} */
16
5
  code
17
- /** @type {string} */
6
+
7
+ /** @type {number|undefined} */
8
+ httpStatusCode
9
+
10
+ /** @type {string|undefined} */
18
11
  httpStatusText
19
12
 
20
- /** @type {Array} */
21
- details
22
- constructor({
23
- httpStatusCode,
24
- code,
25
- httpStatusText,
26
- details = [],
27
- } = DEFAULT_ERROR) {
28
- super(`${code || httpStatus[httpStatusCode]}`)
29
- this.httpStatusCode = httpStatusCode
13
+ /**
14
+ * @param {object} [error]
15
+ * @param {string} [error.code]
16
+ * @param {string} [error.message]
17
+ * @param {number} [error.httpStatusCode]
18
+ * @param {string} [error.httpStatusText]
19
+ */
20
+ constructor(error = {}) {
21
+ const { code, message, httpStatusCode, httpStatusText } = error
22
+
23
+ super(
24
+ message ||
25
+ (httpStatusCode && httpStatus[httpStatusCode]) ||
26
+ code ||
27
+ 'Unknown error',
28
+ )
29
+
30
30
  this.code = code
31
- this.httpStatusText = httpStatusText
32
- this.details = details
31
+ this.httpStatusCode = httpStatusCode
32
+ this.httpStatusText =
33
+ httpStatusText || (httpStatusCode && httpStatus[httpStatusCode])
33
34
 
34
35
  if (typeof Error.captureStackTrace === 'function') {
35
36
  Error.captureStackTrace(this, this.constructor)
36
37
  }
37
38
  }
38
39
 
39
- /**
40
- *
41
- * @param {object} instance
42
- * @returns {boolean}
43
- */
44
40
  static isInstanceOf(instance) {
45
41
  return instance instanceof HttpError
46
42
  }
43
+
44
+ static [Symbol.hasInstance](instance) {
45
+ return (
46
+ instance &&
47
+ typeof instance === 'object' &&
48
+ 'message' in instance &&
49
+ 'httpStatusCode' in instance &&
50
+ 'httpStatusText' in instance
51
+ )
52
+ }
53
+
54
+ toJSON() {
55
+ return {
56
+ code: this.code,
57
+ message: this.message,
58
+ httpStatusCode: this.httpStatusCode,
59
+ httpStatusText: this.httpStatusText,
60
+ }
61
+ }
62
+
63
+ static FromError(error) {
64
+ const httpError = new HttpError({
65
+ code: error.code || 'UNHANDLED_ERROR',
66
+ message: error.message || 'An unexpected error occurred',
67
+ httpStatusCode: httpStatus.INTERNAL_SERVER_ERROR,
68
+ httpStatusText: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
69
+ })
70
+
71
+ httpError.stack = error.stack
72
+ return httpError
73
+ }
47
74
  }
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './fastify'
1
2
  export * from './mongodb/index.js'
2
3
  export * from './mongodb/connect.js'
3
4
  export * from './rabbit-mq/index.js'
@@ -34,7 +34,7 @@ import { mongoConnect } from './connect.js'
34
34
  *
35
35
  * await client.close(); // Close connection manually
36
36
  */
37
- export const initializeMongoDb = async ({ config, collectionNames }) => {
37
+ export const initializeMongoDb = async ({ config, collectionNames = {} }) => {
38
38
  const client = await mongoConnect(config)
39
39
  const db = client.db(config.options.dbName)
40
40
 
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { HttpError } from '../src/http/HttpError.js'
3
+ import httpStatus from 'http-status'
4
+
5
+ describe('HttpError', () => {
6
+ it('creates an error with all fields explicitly set', () => {
7
+ const err = new HttpError({
8
+ code: 'SOME_ERROR',
9
+ message: 'Something went wrong',
10
+ httpStatusCode: httpStatus.BAD_REQUEST,
11
+ httpStatusText: httpStatus[httpStatus.BAD_REQUEST],
12
+ })
13
+
14
+ expect(err).toBeInstanceOf(HttpError)
15
+ expect(err.message).toBe('Something went wrong')
16
+ expect(err.code).toBe('SOME_ERROR')
17
+ expect(err.httpStatusCode).toBe(httpStatus.BAD_REQUEST)
18
+ expect(err.httpStatusText).toBe(httpStatus[httpStatus.BAD_REQUEST])
19
+ })
20
+
21
+ it('falls back to code if message and httpStatusCode are missing', () => {
22
+ const err = new HttpError({ code: 'MY_CODE' })
23
+ expect(err.message).toBe('MY_CODE')
24
+ expect(err.code).toBe('MY_CODE')
25
+ })
26
+
27
+ it('falls back to status text if only httpStatusCode is provided', () => {
28
+ const err = new HttpError({ httpStatusCode: httpStatus.NOT_FOUND })
29
+ expect(err.message).toBe(httpStatus[httpStatus.NOT_FOUND])
30
+ expect(err.httpStatusText).toBe(httpStatus[httpStatus.NOT_FOUND])
31
+ expect(err.httpStatusCode).toBe(httpStatus.NOT_FOUND)
32
+ })
33
+
34
+ it('falls back to "Unknown error" if nothing is provided', () => {
35
+ const err = new HttpError()
36
+ expect(err.message).toBe('Unknown error')
37
+ expect(err.code).toBeUndefined()
38
+ expect(err.httpStatusCode).toBeUndefined()
39
+ expect(err.httpStatusText).toBeUndefined()
40
+ })
41
+
42
+ it('uses httpStatusText if not explicitly set but httpStatusCode is present', () => {
43
+ const err = new HttpError({ httpStatusCode: httpStatus.UNAUTHORIZED })
44
+ expect(err.httpStatusText).toBe(httpStatus[httpStatus.UNAUTHORIZED])
45
+ })
46
+
47
+ it('supports static isInstanceOf check', () => {
48
+ const err = new HttpError()
49
+ expect(HttpError.isInstanceOf(err)).toBe(true)
50
+ expect(HttpError.isInstanceOf(new Error())).toBe(false)
51
+ })
52
+
53
+ it('supports Symbol.hasInstance (dynamic instanceof)', () => {
54
+ const fake = {
55
+ code: 'FAKE',
56
+ message: 'Fake error',
57
+ httpStatusCode: httpStatus.UNAUTHORIZED,
58
+ httpStatusText: httpStatus[httpStatus.UNAUTHORIZED],
59
+ }
60
+
61
+ expect(fake instanceof HttpError).toBe(true)
62
+ })
63
+
64
+ it('serializes correctly via toJSON', () => {
65
+ const err = new HttpError({
66
+ code: 'TOO_FAST',
67
+ message: 'You are too fast',
68
+ httpStatusCode: httpStatus.TOO_MANY_REQUESTS,
69
+ httpStatusText: httpStatus[httpStatus.TOO_MANY_REQUESTS],
70
+ })
71
+
72
+ const json = JSON.stringify(err)
73
+ expect(JSON.parse(json)).toEqual({
74
+ code: 'TOO_FAST',
75
+ message: 'You are too fast',
76
+ httpStatusCode: httpStatus.TOO_MANY_REQUESTS,
77
+ httpStatusText: httpStatus[httpStatus.TOO_MANY_REQUESTS],
78
+ })
79
+ })
80
+ })
@@ -0,0 +1,124 @@
1
+ import httpStatus from 'http-status'
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
3
+
4
+ import { HttpError } from '../src/http/HttpError.js'
5
+ import { GENERAL_ERROR } from '../src/fastify/error-codes.js'
6
+ import {
7
+ replyOnErrorOnly,
8
+ withErrorHandling,
9
+ withErrorHandlingReply,
10
+ } from '../src/fastify/error-handlers/with-error-handling.js'
11
+
12
+ describe('withErrorHandling', () => {
13
+ const log = { error: vi.fn() }
14
+
15
+ it('should return result on success', async () => {
16
+ const fn = vi.fn().mockResolvedValue('ok')
17
+ const result = await withErrorHandling(
18
+ log,
19
+ new HttpError(GENERAL_ERROR),
20
+ )(fn)
21
+ expect(result).toBe('ok')
22
+ })
23
+
24
+ it('should rethrow HttpError', async () => {
25
+ const err = new HttpError({
26
+ message: 'Bad',
27
+ httpStatusCode: httpStatus.BAD_REQUEST,
28
+ })
29
+ const fn = vi.fn().mockRejectedValue(err)
30
+
31
+ await expect(
32
+ withErrorHandling(log, new HttpError(GENERAL_ERROR))(fn),
33
+ ).rejects.toThrow(err)
34
+ })
35
+
36
+ it('should wrap non-HttpError with HttpError', async () => {
37
+ const fn = vi.fn().mockRejectedValue(new Error('oops'))
38
+
39
+ await expect(
40
+ withErrorHandling(log, new HttpError(GENERAL_ERROR))(fn),
41
+ ).rejects.toBeInstanceOf(HttpError)
42
+ })
43
+ })
44
+
45
+ describe('withErrorHandlingReply', () => {
46
+ const reply = {
47
+ status: vi.fn().mockReturnThis(),
48
+ send: vi.fn(),
49
+ }
50
+
51
+ const log = { error: vi.fn() }
52
+
53
+ beforeEach(() => {
54
+ vi.clearAllMocks()
55
+ })
56
+
57
+ it('should return result on success', async () => {
58
+ const fn = vi.fn().mockResolvedValue('good')
59
+ const result = await withErrorHandlingReply({ reply, log })(fn)
60
+ expect(result).toBe('good')
61
+ })
62
+
63
+ it('should send reply on HttpError', async () => {
64
+ const err = new HttpError({
65
+ message: 'not found',
66
+ code: 'NOT_FOUND',
67
+ httpStatusCode: httpStatus.NOT_FOUND,
68
+ httpStatusText: httpStatus[httpStatus.NOT_FOUND],
69
+ })
70
+
71
+ const fn = vi.fn().mockRejectedValue(err)
72
+ await withErrorHandlingReply({ reply, log })(fn)
73
+
74
+ expect(reply.status).toHaveBeenCalledWith(httpStatus.NOT_FOUND)
75
+ expect(reply.send).toHaveBeenCalledWith({
76
+ code: 'NOT_FOUND',
77
+ httpStatusText: 'Not Found',
78
+ })
79
+ })
80
+ })
81
+
82
+ describe('replyOnErrorOnly', () => {
83
+ const reply = {
84
+ status: vi.fn().mockReturnThis(),
85
+ send: vi.fn(),
86
+ }
87
+
88
+ const log = { error: vi.fn() }
89
+
90
+ beforeEach(() => {
91
+ vi.clearAllMocks()
92
+ })
93
+
94
+ it('should return result on success', async () => {
95
+ const fn = vi.fn().mockResolvedValue('yay')
96
+ const result = await replyOnErrorOnly({ reply, log })(fn)
97
+ expect(result).toBe('yay')
98
+ })
99
+
100
+ it('should send reply with default error when error is not HttpError', async () => {
101
+ const fn = vi.fn().mockRejectedValue(new Error('bad'))
102
+ await replyOnErrorOnly({ reply, log })(fn)
103
+
104
+ expect(reply.status).toHaveBeenCalledWith(httpStatus.INTERNAL_SERVER_ERROR)
105
+ expect(reply.send).toHaveBeenCalledWith(
106
+ expect.objectContaining({ error: 'bad' }),
107
+ )
108
+ })
109
+
110
+ it('should send reply with HttpError if thrown', async () => {
111
+ const err = new HttpError({
112
+ message: 'forbidden',
113
+ code: 'NO_ACCESS',
114
+ httpStatusCode: httpStatus.FORBIDDEN,
115
+ httpStatusText: httpStatus[httpStatus.FORBIDDEN],
116
+ })
117
+
118
+ const fn = vi.fn().mockRejectedValue(err)
119
+ await replyOnErrorOnly({ reply, log })(fn)
120
+
121
+ expect(reply.status).toHaveBeenCalledWith(httpStatus.FORBIDDEN)
122
+ expect(reply.send).toHaveBeenCalledWith({ error: 'forbidden' })
123
+ })
124
+ })