@mojaloop/central-services-shared 18.14.2 → 18.15.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [18.15.0](https://github.com/mojaloop/central-services-shared/compare/v18.14.2...v18.15.0) (2025-01-06)
6
+
7
+
8
+ ### Features
9
+
10
+ * add rethrow functions ([#425](https://github.com/mojaloop/central-services-shared/issues/425)) ([82758d4](https://github.com/mojaloop/central-services-shared/commit/82758d4a3ffcd9cefadfcd1d22b5e2dab6aa3358))
11
+
5
12
  ### [18.14.2](https://github.com/mojaloop/central-services-shared/compare/v18.14.1...v18.14.2) (2024-12-20)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/central-services-shared",
3
- "version": "18.14.2",
3
+ "version": "18.15.0",
4
4
  "description": "Shared code for mojaloop central services",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ModusBox",
@@ -68,7 +68,7 @@
68
68
  "event-stream": "4.0.1",
69
69
  "fast-safe-stringify": "^2.1.1",
70
70
  "immutable": "5.0.3",
71
- "ioredis": "^5.4.1",
71
+ "ioredis": "^5.4.2",
72
72
  "lodash": "4.17.21",
73
73
  "mustache": "4.2.0",
74
74
  "openapi-backend": "5.11.1",
@@ -78,7 +78,7 @@
78
78
  "ulidx": "2.4.1",
79
79
  "uuid4": "2.0.3",
80
80
  "widdershins": "^4.0.1",
81
- "yaml": "2.6.1"
81
+ "yaml": "2.7.0"
82
82
  },
83
83
  "devDependencies": {
84
84
  "@hapi/hapi": "21.3.12",
@@ -86,7 +86,7 @@
86
86
  "audit-ci": "^7.1.0",
87
87
  "base64url": "3.0.1",
88
88
  "chance": "1.1.12",
89
- "npm-check-updates": "17.1.11",
89
+ "npm-check-updates": "17.1.13",
90
90
  "nyc": "17.1.0",
91
91
  "portfinder": "1.0.32",
92
92
  "pre-commit": "1.2.2",
package/src/util/index.js CHANGED
@@ -49,6 +49,7 @@ const OpenapiBackend = require('./openapiBackend')
49
49
  const id = require('./id')
50
50
  const config = require('../config')
51
51
  const { loggerFactory } = require('@mojaloop/central-services-logger/src/contextLogger')
52
+ const rethrow = require('./rethrow')
52
53
 
53
54
  const omitNil = (object) => {
54
55
  return _.omitBy(object, _.isNil)
@@ -265,5 +266,6 @@ module.exports = {
265
266
  Schema,
266
267
  OpenapiBackend,
267
268
  id,
268
- resourceVersions: Helpers.resourceVersions
269
+ resourceVersions: Helpers.resourceVersions,
270
+ rethrow
269
271
  }
@@ -38,7 +38,7 @@ const Enum = require('../../enums')
38
38
  const StreamingProtocol = require('../streaming/protocol')
39
39
  const ErrorHandler = require('@mojaloop/central-services-error-handling')
40
40
  const EventSdk = require('@mojaloop/event-sdk')
41
-
41
+ const { rethrowKafkaError } = require('../rethrow')
42
42
  /**
43
43
  * @function ParticipantTopicTemplate
44
44
  *
@@ -59,8 +59,7 @@ const participantTopicTemplate = (template, participantName, functionality, acti
59
59
  action
60
60
  })
61
61
  } catch (err) {
62
- Logger.isErrorEnabled && Logger.error(err)
63
- throw ErrorHandler.Factory.reformatFSPIOPError(err)
62
+ rethrowKafkaError(err)
64
63
  }
65
64
  }
66
65
 
@@ -79,8 +78,7 @@ const generalTopicTemplate = (template, functionality, action) => {
79
78
  try {
80
79
  return Mustache.render(template, { functionality, action })
81
80
  } catch (err) {
82
- Logger.isErrorEnabled && Logger.error(err)
83
- throw ErrorHandler.Factory.reformatFSPIOPError(err)
81
+ rethrowKafkaError(err)
84
82
  }
85
83
  }
86
84
 
@@ -102,8 +100,7 @@ const transformGeneralTopicName = (template, functionality, action) => {
102
100
  }
103
101
  return generalTopicTemplate(template, functionality, action)
104
102
  } catch (err) {
105
- Logger.isErrorEnabled && Logger.error(err)
106
- throw ErrorHandler.Factory.reformatFSPIOPError(err)
103
+ rethrowKafkaError(err)
107
104
  }
108
105
  }
109
106
 
@@ -123,8 +120,7 @@ const transformAccountToTopicName = (template, participantName, functionality, a
123
120
  try {
124
121
  return participantTopicTemplate(template, participantName, functionality, action)
125
122
  } catch (err) {
126
- Logger.isErrorEnabled && Logger.error(err)
127
- throw ErrorHandler.Factory.reformatFSPIOPError(err)
123
+ rethrowKafkaError(err)
128
124
  }
129
125
  }
130
126
 
@@ -148,7 +144,8 @@ const getKafkaConfig = (kafkaConfig, flow, functionality, action) => {
148
144
  actionObject.config.logger = Logger
149
145
  return actionObject.config
150
146
  } catch (err) {
151
- throw ErrorHandler.Factory.createInternalServerFSPIOPError(`No config found for flow='${flow}', functionality='${functionality}', action='${action}'`, err)
147
+ const error = ErrorHandler.Factory.createInternalServerFSPIOPError(`No config found for flow='${flow}', functionality='${functionality}', action='${action}'`, err)
148
+ rethrowKafkaError(error)
152
149
  }
153
150
  }
154
151
 
@@ -280,8 +277,7 @@ const commitMessageSync = async (kafkaConsumer, kafkaTopic, message) => {
280
277
  await consumer.commitMessageSync(message)
281
278
  } catch (err) {
282
279
  Logger.isDebugEnabled && Logger.debug(`No consumer found for topic ${kafkaTopic}`)
283
- Logger.isErrorEnabled && Logger.error(err)
284
- throw err
280
+ rethrowKafkaError(err)
285
281
  }
286
282
  }
287
283
  }
@@ -307,7 +303,8 @@ const proceed = async (defaultKafkaConfig, params, opts) => {
307
303
  message.value.to = message.value.from
308
304
 
309
305
  if (!opts.hubName) {
310
- throw ErrorHandler.Factory.createInternalServerFSPIOPError('No hubName found in opts')
306
+ const error = ErrorHandler.Factory.createInternalServerFSPIOPError('No hubName found in opts')
307
+ rethrowKafkaError(error)
311
308
  }
312
309
 
313
310
  message.value.from = opts.hubName
@@ -1,8 +1,8 @@
1
1
  const Redis = require('ioredis')
2
2
  const { createLogger } = require('../index')
3
3
  const { REDIS_SUCCESS, REDIS_IS_CONNECTED_STATUSES } = require('../../constants')
4
-
5
4
  const isClusterConfig = (config) => { return 'cluster' in config }
5
+ const { rethrowRedisError } = require('../rethrow')
6
6
 
7
7
  class RedisCache {
8
8
  constructor (config, client) {
@@ -78,7 +78,7 @@ class RedisCache {
78
78
  return await this.redisClient.get(key)
79
79
  } catch (err) {
80
80
  this.log.error('Error getting key from Redis:', err)
81
- throw err
81
+ rethrowRedisError(err)
82
82
  }
83
83
  }
84
84
 
@@ -91,7 +91,7 @@ class RedisCache {
91
91
  }
92
92
  } catch (err) {
93
93
  this.log.error('Error setting key in Redis:', err)
94
- throw err
94
+ rethrowRedisError(err)
95
95
  }
96
96
  }
97
97
 
@@ -100,7 +100,7 @@ class RedisCache {
100
100
  await this.redisClient.del(key)
101
101
  } catch (err) {
102
102
  this.log.error('Error deleting key from Redis:', err)
103
- throw err
103
+ rethrowRedisError(err)
104
104
  }
105
105
  }
106
106
 
@@ -112,7 +112,7 @@ class RedisCache {
112
112
  await pipeline.exec()
113
113
  } catch (err) {
114
114
  this.log.error('Error clearing Redis cache:', err)
115
- throw err
115
+ rethrowRedisError(err)
116
116
  }
117
117
  }
118
118
  }
@@ -0,0 +1,112 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2024 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+ http://www.apache.org/licenses/LICENSE-2.0
7
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
8
+ Contributors
9
+ --------------
10
+ This is the official list of the Mojaloop project contributors for this file.
11
+ Names of the original copyright holders (individuals or organizations)
12
+ should be listed with a '*' in the first column. People who have
13
+ contributed from an organization can be listed under the organization
14
+ that actually holds the copyright for their contributions (see the
15
+ Mojaloop Foundation for an example). Those individuals should have
16
+ their names indented and be marked with a '-'. Email address can be added
17
+ optionally within square brackets <email>.
18
+ * Mojaloop Foundation
19
+ - Name Surname <name.surname@mojaloop.io>
20
+ * Infitx
21
+ - Kevin Leyow <kevin.leyow@infitx.com>
22
+ --------------
23
+ ******/
24
+
25
+ const ErrorHandler = require('@mojaloop/central-services-error-handling')
26
+ const Metrics = require('@mojaloop/central-services-metrics')
27
+
28
+ const { logger } = require('../logger')
29
+
30
+ /**
31
+ * Rethrows an FSPIOP error after logging it and incrementing an error counter.
32
+ *
33
+ * @param {Error} error - The error to be rethrown.
34
+ * @param {Object} [options={}] - Optional parameters.
35
+ * @param {string} [options.operation] - The operation during which the error occurred.
36
+ * @param {string} [options.step] - The step during which the error occurred.
37
+ * @param {Object} [options.loggerOverride] - An optional logger to override the default logger.
38
+ * @throws {Error} - Throws the reformatted FSPIOP error.
39
+ */
40
+ const rethrowAndCountFspiopError = (error, options = {}) => {
41
+ const { operation, step, loggerOverride } = options
42
+ const log = loggerOverride || logger
43
+ log.error(`rethrow fspiop error: ${error?.message}`)
44
+
45
+ const fspiopError = ErrorHandler.Factory.reformatFSPIOPError(error)
46
+ const extensions = fspiopError.extensions || []
47
+ const system = extensions.find((element) => element.key === 'system')?.value || ''
48
+
49
+ try {
50
+ const errorCounter = Metrics.getCounter('errorCount')
51
+ errorCounter.inc({
52
+ code: fspiopError?.apiErrorCode.code,
53
+ system,
54
+ operation,
55
+ step
56
+ })
57
+ } catch (error) {
58
+ log.error('Metrics error counter not initialized', error)
59
+ }
60
+
61
+ throw fspiopError
62
+ }
63
+
64
+ const constructSystemExtensionError = (error, system) => {
65
+ const extensions = [{
66
+ key: 'system',
67
+ value: system
68
+ }]
69
+ return ErrorHandler.Factory.reformatFSPIOPError(
70
+ error,
71
+ undefined,
72
+ undefined,
73
+ extensions
74
+ )
75
+ }
76
+
77
+ const rethrowError = (error, options = {}, system) => {
78
+ const { loggerOverride } = options
79
+ const log = loggerOverride || logger
80
+ log.error(`rethrow fspiop error: ${error?.message}`)
81
+ throw constructSystemExtensionError(error, system)
82
+ }
83
+
84
+ const rethrowDatabaseError = (error, options = {}) => {
85
+ rethrowError(error, options, '["db"]')
86
+ }
87
+
88
+ const rethrowCachedDatabaseError = (error, options = {}) => {
89
+ rethrowError(error, options, '["db","cache"]')
90
+ }
91
+
92
+ const rethrowRedisError = (error, options = {}) => {
93
+ rethrowError(error, options, '["redis"]')
94
+ }
95
+
96
+ const rethrowKafkaError = (error, options = {}) => {
97
+ rethrowError(error, options, '["kafka"]')
98
+ }
99
+
100
+ const rethrowCacheError = (error, options = {}) => {
101
+ rethrowError(error, options, '["cache"]')
102
+ }
103
+
104
+ module.exports = {
105
+ rethrowAndCountFspiopError,
106
+ rethrowDatabaseError,
107
+ rethrowCachedDatabaseError,
108
+ rethrowRedisError,
109
+ rethrowKafkaError,
110
+ rethrowCacheError,
111
+ constructSystemExtensionError
112
+ }
@@ -1,6 +1,7 @@
1
1
  const Test = require('tapes')(require('tape'))
2
2
  const sinon = require('sinon')
3
3
  const RedisCache = require('../../../../src/util/redis/redisCache')
4
+ const { constructSystemExtensionError } = require('../../../../src/util/rethrow')
4
5
 
5
6
  Test('RedisCache', redisCacheTest => {
6
7
  let sandbox, redisClientStub, redisCache
@@ -118,7 +119,7 @@ Test('RedisCache', redisCacheTest => {
118
119
  await redisCache.get('key')
119
120
  t.fail('Expected error to be thrown')
120
121
  } catch (err) {
121
- t.equal(err, error, 'Error thrown and rethrown correctly')
122
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
122
123
  }
123
124
  t.end()
124
125
  })
@@ -130,7 +131,7 @@ Test('RedisCache', redisCacheTest => {
130
131
  await redisCache.set('key', 'value', 60)
131
132
  t.fail('Expected error to be thrown')
132
133
  } catch (err) {
133
- t.equal(err, error, 'Error thrown and rethrown correctly')
134
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
134
135
  }
135
136
  t.end()
136
137
  })
@@ -142,7 +143,7 @@ Test('RedisCache', redisCacheTest => {
142
143
  await redisCache.delete('key')
143
144
  t.fail('Expected error to be thrown')
144
145
  } catch (err) {
145
- t.equal(err, error, 'Error thrown and rethrown correctly')
146
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
146
147
  }
147
148
  t.end()
148
149
  })
@@ -154,7 +155,7 @@ Test('RedisCache', redisCacheTest => {
154
155
  await redisCache.clearCache()
155
156
  t.fail('Expected error to be thrown')
156
157
  } catch (err) {
157
- t.equal(err, error, 'Error thrown and rethrown correctly')
158
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
158
159
  }
159
160
  t.end()
160
161
  })
@@ -0,0 +1,146 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2024 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+ http://www.apache.org/licenses/LICENSE-2.0
7
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
8
+ Contributors
9
+ --------------
10
+ This is the official list of the Mojaloop project contributors for this file.
11
+ Names of the original copyright holders (individuals or organizations)
12
+ should be listed with a '*' in the first column. People who have
13
+ contributed from an organization can be listed under the organization
14
+ that actually holds the copyright for their contributions (see the
15
+ Mojaloop Foundation for an example). Those individuals should have
16
+ their names indented and be marked with a '-'. Email address can be added
17
+ optionally within square brackets <email>.
18
+ * Mojaloop Foundation
19
+ - Name Surname <name.surname@mojaloop.io>
20
+ * Infitx
21
+ - Kevin Leyow <kevin.leyow@infitx.com>
22
+ --------------
23
+ ******/
24
+
25
+ const Test = require('tapes')(require('tape'))
26
+ const sinon = require('sinon')
27
+ const rethrow = require('../../../src/util/rethrow')
28
+ const ErrorHandler = require('@mojaloop/central-services-error-handling')
29
+
30
+ Test('rethrow.js', rethrowTest => {
31
+ let sandbox
32
+
33
+ rethrowTest.beforeEach(t => {
34
+ sandbox = sinon.createSandbox()
35
+ sandbox.stub(ErrorHandler.Factory, 'reformatFSPIOPError')
36
+ t.end()
37
+ })
38
+
39
+ rethrowTest.afterEach(t => {
40
+ sandbox.restore()
41
+ t.end()
42
+ })
43
+
44
+ rethrowTest.test('rethrowAndCountFspiopError should log the error and rethrow a reformatted FSPIOP error', t => {
45
+ const error = new Error('Test error')
46
+ const fspiopError = new Error('FSPIOP error')
47
+ ErrorHandler.Factory.reformatFSPIOPError.returns(fspiopError)
48
+
49
+ try {
50
+ rethrow.rethrowAndCountFspiopError(error)
51
+ t.fail('Expected error to be thrown')
52
+ } catch (err) {
53
+ t.equal(err, fspiopError, 'Error rethrown correctly')
54
+ }
55
+ t.end()
56
+ })
57
+
58
+ rethrowTest.test('rethrowAndCountFspiopError should increment the error counter with correct labels', t => {
59
+ const error = new Error('Test error')
60
+ const fspiopError = {
61
+ apiErrorCode: { code: '1234' },
62
+ extensions: [{ key: 'system', value: 'testSystem' }]
63
+ }
64
+ ErrorHandler.Factory.reformatFSPIOPError.returns(fspiopError)
65
+
66
+ try {
67
+ rethrow.rethrowAndCountFspiopError(error, { operation: 'testOp', step: 'testStep' })
68
+ t.fail('Expected error to be thrown')
69
+ } catch (err) {
70
+ t.equal(err, fspiopError, 'Error rethrown correctly')
71
+ }
72
+ t.end()
73
+ })
74
+
75
+ rethrowTest.test('rethrowDatabaseError should rethrow a database error', t => {
76
+ const error = new Error('Database error')
77
+ const systemError = new Error('System error')
78
+ ErrorHandler.Factory.reformatFSPIOPError.returns(systemError)
79
+
80
+ try {
81
+ rethrow.rethrowDatabaseError(error)
82
+ t.fail('Expected error to be thrown')
83
+ } catch (err) {
84
+ t.equal(err, systemError, 'Error rethrown correctly')
85
+ }
86
+ t.end()
87
+ })
88
+
89
+ rethrowTest.test('rethrowCachedDatabaseError should rethrow a cached database error', t => {
90
+ const error = new Error('Cached database error')
91
+ const systemError = new Error('System error')
92
+ ErrorHandler.Factory.reformatFSPIOPError.returns(systemError)
93
+
94
+ try {
95
+ rethrow.rethrowCachedDatabaseError(error)
96
+ t.fail('Expected error to be thrown')
97
+ } catch (err) {
98
+ t.equal(err, systemError, 'Error rethrown correctly')
99
+ }
100
+ t.end()
101
+ })
102
+
103
+ rethrowTest.test('rethrowRedisError should rethrow a redis error', t => {
104
+ const error = new Error('Redis error')
105
+ const systemError = new Error('System error')
106
+ ErrorHandler.Factory.reformatFSPIOPError.returns(systemError)
107
+
108
+ try {
109
+ rethrow.rethrowRedisError(error)
110
+ t.fail('Expected error to be thrown')
111
+ } catch (err) {
112
+ t.equal(err, systemError, 'Error rethrown correctly')
113
+ }
114
+ t.end()
115
+ })
116
+
117
+ rethrowTest.test('rethrowKafkaError should rethrow a kafka error', t => {
118
+ const error = new Error('Kafka error')
119
+ const systemError = new Error('System error')
120
+ ErrorHandler.Factory.reformatFSPIOPError.returns(systemError)
121
+
122
+ try {
123
+ rethrow.rethrowKafkaError(error)
124
+ t.fail('Expected error to be thrown')
125
+ } catch (err) {
126
+ t.equal(err, systemError, 'Error rethrown correctly')
127
+ }
128
+ t.end()
129
+ })
130
+
131
+ rethrowTest.test('rethrowCacheError should rethrow a cache error', t => {
132
+ const error = new Error('Cache error')
133
+ const systemError = new Error('System error')
134
+ ErrorHandler.Factory.reformatFSPIOPError.returns(systemError)
135
+
136
+ try {
137
+ rethrow.rethrowCacheError(error)
138
+ t.fail('Expected error to be thrown')
139
+ } catch (err) {
140
+ t.equal(err, systemError, 'Error rethrown correctly')
141
+ }
142
+ t.end()
143
+ })
144
+
145
+ rethrowTest.end()
146
+ })