@mojaloop/central-services-shared 18.26.2 → 18.27.0-snapshot.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/central-services-shared",
3
- "version": "18.26.2",
3
+ "version": "18.27.0-snapshot.0",
4
4
  "description": "Shared code for mojaloop central services",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ModusBox",
@@ -43,6 +43,7 @@
43
43
  "test": "npm run test:unit",
44
44
  "test:header": "npx tape 'test/unit/util/headerValidation/**/*.test.js'",
45
45
  "test:logging": "npx tape 'test/unit/util/hapi/plugins/loggingPlugin.test.js'",
46
+ "test:mysql": "tape 'test/unit/util/mysql/**/*.test.js'",
46
47
  "test:trans": "npx tape 'test/unit/util/headers/transformer.test.js'",
47
48
  "test:unit": "npx tape 'test/unit/**/*.test.js' | tap-spec",
48
49
  "test:xunit": "npx tape 'test/unit/**/**.test.js' | tap-xunit > ./test/results/xunit.xml",
@@ -65,6 +66,8 @@
65
66
  "@hapi/joi-date": "2.0.1",
66
67
  "@mojaloop/inter-scheme-proxy-cache-lib": "2.5.0",
67
68
  "@opentelemetry/api": "1.9.0",
69
+ "async-exit-hook": "2.0.1",
70
+ "async-retry": "1.3.3",
68
71
  "axios": "1.9.0",
69
72
  "clone": "2.1.2",
70
73
  "convict": "^6.2.4",
@@ -143,7 +146,9 @@
143
146
  "@mojaloop/event-sdk": "14.5.1",
144
147
  "ajv": "8.x.x",
145
148
  "ajv-formats": "3.x.x",
146
- "ajv-keywords": "5.x.x"
149
+ "ajv-keywords": "5.x.x",
150
+ "knex": "3.x",
151
+ "mysql2": "3.x"
147
152
  },
148
153
  "peerDependenciesMeta": {
149
154
  "@mojaloop/central-services-error-handling": {
@@ -163,6 +168,12 @@
163
168
  },
164
169
  "ajv-keyboards": {
165
170
  "optional": false
171
+ },
172
+ "knex": {
173
+ "optional": false
174
+ },
175
+ "mysql2": {
176
+ "optional": false
166
177
  }
167
178
  },
168
179
  "standard": {
package/src/index.d.ts CHANGED
@@ -1,7 +1,18 @@
1
1
  import { Utils as HapiUtil, Server } from '@hapi/hapi'
2
2
  import { ILogger } from '@mojaloop/central-services-logger/src/contextLogger'
3
+ import { Knex } from 'knex';
3
4
  import IORedis from 'ioredis';
4
5
 
6
+ declare class KnexWrapper {
7
+ constructor(deps: KnexWrapperDeps);
8
+ knex: Knex;
9
+ isConnected: boolean;
10
+ connect(): Promise<void>;
11
+ disconnect(): Promise<void>;
12
+ executeWithErrorCount(queryFn: Function, operation?: string, step?: string): Promise<unknown>;
13
+ handleError(error: unknown, operation?: string, step?: string, needRethrow?: boolean): void;
14
+ }
15
+
5
16
  declare namespace CentralServicesShared {
6
17
  interface ReturnCode {
7
18
  CODE: number;
@@ -422,6 +433,7 @@ declare namespace CentralServicesShared {
422
433
  enum AdminNotificationActionsEnum {
423
434
  LIMIT_ADJUSTMENT = 'limit-adjustment'
424
435
  }
436
+
425
437
  interface Enum {
426
438
  Http: HttpEnum;
427
439
  EndPoints: EndPointsEnum;
@@ -778,6 +790,9 @@ declare namespace CentralServicesShared {
778
790
  StreamingProtocol: StreamingProtocol;
779
791
  HeaderValidation: HeaderValidation;
780
792
  Redis: Redis;
793
+ mysql: {
794
+ KnexWrapper: KnexWrapper
795
+ };
781
796
  }
782
797
 
783
798
  const Enum: Enum
@@ -785,4 +800,27 @@ declare namespace CentralServicesShared {
785
800
  const HealthCheck: any
786
801
  }
787
802
 
803
+ type KnexWrapperDeps = {
804
+ knexOptions: Knex.Config;
805
+ metrics: MetricsClient;
806
+ logger: ILogger;
807
+ retryOptions?: RetryConnOptions;
808
+ context?: string;
809
+ }
810
+
811
+ type MetricsClient = { // Wrapper for prom-client from @mojaloop/central-services-metrics
812
+ getCounter(name: string): {
813
+ inc(details: Record<string, unknown>): void;
814
+ };
815
+ }
816
+
817
+ interface RetryConnOptions { // see opts from https://www.npmjs.com/package/async-retry#api
818
+ retries?: number;
819
+ factor?: number;
820
+ minTimeout?: number;
821
+ maxTimeout?: number;
822
+ randomize?: boolean;
823
+ onRetry?: (error: Error) => void;
824
+ }
825
+
788
826
  export = CentralServicesShared
@@ -0,0 +1,146 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 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
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ 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.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
28
+ /* istanbul ignore file */
29
+ // todo: improve test coverage
30
+
31
+ const knex = require('knex')
32
+ const retry = require('async-retry')
33
+ const exitHook = require('async-exit-hook')
34
+
35
+ const defaultRetryOptionsDto = (logger) => ({
36
+ retries: 10,
37
+ minTimeout: 1000,
38
+ factor: 2,
39
+ onRetry: (err) => { logger.info('Database connection attempt: ', err) }
40
+ })
41
+
42
+ /**
43
+ * @typedef {Object} KnexWrapperDeps
44
+ * @prop {Object} knexOptions - Configuration for
45
+ * @prop {Object} [retryOptions] - Configuration for async-retry package
46
+ * @prop {Metrics} metrics - Wrapper for prom-client from @mojaloop/central-services-metrics
47
+ * @prop {ILogger} logger - Logger instance
48
+ * @prop {string} [context] - Context string for errorCunter
49
+ */
50
+
51
+ class KnexWrapper {
52
+ #isConnected = false
53
+
54
+ /** @param {KnexWrapperDeps} deps */
55
+ constructor (deps) {
56
+ this.knex = knex(deps.knexOptions)
57
+ this.metrics = deps.metrics
58
+ this.log = deps.logger.child({ component: this.constructor.name })
59
+ this.retryOptions = Object.assign(defaultRetryOptionsDto(this.log), deps.retryOptions)
60
+ this.context = deps.context || 'Knex'
61
+ }
62
+
63
+ get isConnected () { return this.#isConnected }
64
+
65
+ async connect () {
66
+ try {
67
+ const opts = this.retryOptions
68
+ await retry(async (_, attempt) => {
69
+ this.log.verbose(`Attempting database connection. Attempt ${attempt} of ${opts.retries + 1}`)
70
+ await this.knex.raw('SELECT 1')
71
+ exitHook(callback => {
72
+ this.knex.destroy().finally(callback)
73
+ })
74
+ this.#isConnected = true
75
+ this.log.info('Database connected')
76
+ }, opts)
77
+ } catch (err) {
78
+ this.log.error('error connecting to DB: ', err)
79
+ this.handleError(err, 'connect')
80
+ }
81
+ }
82
+
83
+ async disconnect () {
84
+ await this.knex.destroy()
85
+ this.#isConnected = false
86
+ this.log.info('Database disconnected')
87
+ }
88
+
89
+ async executeWithErrorCount (queryFn, operation = queryFn.name || 'runQuery', step = '') {
90
+ try {
91
+ if (!this.isConnected) this.log.warn('Database is not connected')
92
+ const result = await queryFn(this.knex)
93
+ this.log.debug('executeWithErrorCount is done: ', { result })
94
+ return result
95
+ } catch (err) {
96
+ this.log.error('error in executeWithErrorCount: ', err)
97
+ this.handleError(err, operation, step)
98
+ }
99
+ }
100
+
101
+ handleError (error, operation, step = '', needRethrow = true) {
102
+ const code = this.#defineErrorCode(error)
103
+ this.#incrementErrorCounter({ code, operation, step })
104
+ if (needRethrow) throw error
105
+ else return null
106
+ }
107
+
108
+ #defineErrorCode (error) {
109
+ if (error instanceof knex.KnexTimeoutError) {
110
+ return 'conn_timeout'
111
+ }
112
+ if (error.code === 'ECONNREFUSED') {
113
+ return 'conn_failed'
114
+ }
115
+ if (error.code === 'ER_LOCK_DEADLOCK') {
116
+ return 'deadlock'
117
+ }
118
+ if (error.code === 'ER_DUP_ENTRY') {
119
+ return 'dup_entry'
120
+ }
121
+ if (!this.isConnected) {
122
+ return 'not_connected'
123
+ }
124
+ return 'unknown_db_error'
125
+ }
126
+
127
+ #incrementErrorCounter ({ code, operation = 'db_query', step = '' }) {
128
+ const { log, context } = this
129
+ try {
130
+ const errorCounter = this.metrics.getCounter('errorCount')
131
+ const errDetails = {
132
+ system: 'mysql',
133
+ code,
134
+ context,
135
+ operation,
136
+ ...(step && { step })
137
+ }
138
+ errorCounter.inc(errDetails)
139
+ log.info('incrementErrorCounter is called:', { errDetails })
140
+ } catch (error) {
141
+ log.warn('error in incrementErrorCounter: ', error)
142
+ }
143
+ }
144
+ }
145
+
146
+ module.exports = KnexWrapper
@@ -0,0 +1,32 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 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
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ 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.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
28
+ const KnexWrapper = require('./KnexWrapper')
29
+
30
+ module.exports = {
31
+ KnexWrapper
32
+ }
@@ -0,0 +1,74 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 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
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ 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.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
28
+ const Tape = require('tapes')(require('tape'))
29
+ // const sinon = require('sinon')
30
+
31
+ const { KnexWrapper } = require('#src/util/mysql/index')
32
+ const { logger } = require('#src/logger')
33
+ const { tryCatchEndTest } = require('#test/util/helper')
34
+
35
+ const mockDeps = ({
36
+ knexOptions = mockKnexOptions(),
37
+ retryOptions = mockRetryOptions(),
38
+ metrics = {
39
+ getCounter: () => ({
40
+ inc: () => {} // todo: use stub
41
+ })
42
+ },
43
+ context = 'testKnexWrapper'
44
+ } = {}) => ({
45
+ logger,
46
+ knexOptions,
47
+ metrics,
48
+ retryOptions,
49
+ context
50
+ })
51
+
52
+ const mockKnexOptions = () => ({
53
+ client: 'mysql2',
54
+ connection: {
55
+ host: '127.0.0.1',
56
+ user: 'root',
57
+ password: '<PASSWORD>',
58
+ database: 'test'
59
+ }
60
+ })
61
+
62
+ const mockRetryOptions = ({
63
+ retries = 1,
64
+ minTimeout = 100
65
+ } = {}) => ({ retries, minTimeout })
66
+
67
+ Tape('KnexWrapper Tests -->', (wrapperTests) => {
68
+ wrapperTests.test('should create an instance', tryCatchEndTest(t => {
69
+ const wrapper = new KnexWrapper(mockDeps())
70
+ t.false(wrapper.isConnected, 'wrapper is not connected')
71
+ }))
72
+
73
+ wrapperTests.end()
74
+ })