@mojaloop/database-lib 11.2.0 → 11.3.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,15 @@
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
+ ## [11.3.0](https://github.com/mojaloop/database-lib/compare/v11.2.1...v11.3.0) (2025-07-14)
6
+
7
+
8
+ ### Features
9
+
10
+ * fix vul and update deps ([#172](https://github.com/mojaloop/database-lib/issues/172)) ([a0d969f](https://github.com/mojaloop/database-lib/commit/a0d969f94b77709a2b9712b94cf52e62961a8796))
11
+
12
+ ### [11.2.1](https://github.com/mojaloop/database-lib/compare/v11.2.0...v11.2.1) (2025-07-08)
13
+
5
14
  ## [11.2.0](https://github.com/mojaloop/database-lib/compare/v11.1.4...v11.2.0) (2025-05-14)
6
15
 
7
16
 
File without changes
@@ -0,0 +1,65 @@
1
+ services:
2
+ cert-gen:
3
+ image: alpine
4
+ volumes:
5
+ - ./certs:/certs
6
+ command: >
7
+ sh -c "
8
+ if [ ! -f /certs/ca.pem ]; then
9
+ echo 'SSL certificates not found. Generating certificates...' &&
10
+ apk add --no-cache openssl &&
11
+ openssl genrsa -out /certs/ca-key.pem 2048 &&
12
+ openssl req -new -x509 -nodes -days 365 -key /certs/ca-key.pem -out /certs/ca.pem -subj '/C=US/ST=CA/L=San Francisco/O=Mojaloop/OU=Database/CN=MySQL_CA' &&
13
+ openssl genrsa -out /certs/server-key.pem 2048 &&
14
+ openssl req -new -key /certs/server-key.pem -out /certs/server.csr -subj '/C=US/ST=CA/L=San Francisco/O=Mojaloop/OU=Database/CN=mysql' &&
15
+ echo '[v3_req]' > /tmp/cert.conf &&
16
+ echo 'basicConstraints = CA:FALSE' >> /tmp/cert.conf &&
17
+ echo 'keyUsage = nonRepudiation, digitalSignature, keyEncipherment' >> /tmp/cert.conf &&
18
+ echo 'subjectAltName = @alt_names' >> /tmp/cert.conf &&
19
+ echo '[alt_names]' >> /tmp/cert.conf &&
20
+ echo 'DNS.1 = mysql' >> /tmp/cert.conf &&
21
+ echo 'DNS.2 = localhost' >> /tmp/cert.conf &&
22
+ echo 'IP.1 = 127.0.0.1' >> /tmp/cert.conf &&
23
+ openssl x509 -req -days 365 -in /certs/server.csr -CA /certs/ca.pem -CAkey /certs/ca-key.pem -CAcreateserial -out /certs/server-cert.pem -extensions v3_req -extfile /tmp/cert.conf &&
24
+ rm -f /certs/*.csr /certs/*.srl /tmp/cert.conf &&
25
+ chmod 644 /certs/ca.pem /certs/server-cert.pem &&
26
+ chmod 644 /certs/ca-key.pem /certs/server-key.pem &&
27
+ echo 'SSL certificates generated successfully!' &&
28
+ echo 'Generated certificate files with permissions:' &&
29
+ ls -la /certs/ &&
30
+ echo 'Certificate files should be readable by any user for CI compatibility.'
31
+ else
32
+ echo 'SSL certificates found. Ready to start MySQL.'
33
+ fi
34
+ "
35
+ restart: "no"
36
+
37
+ mysql:
38
+ image: mysql:8.0
39
+ depends_on:
40
+ cert-gen:
41
+ condition: service_completed_successfully
42
+ environment:
43
+ MYSQL_ROOT_PASSWORD: example_root_password
44
+ MYSQL_DATABASE: example_db
45
+ MYSQL_USER: example_user
46
+ MYSQL_PASSWORD: example_password
47
+ command:
48
+ - --ssl-ca=/etc/mysql/certs/ca.pem
49
+ - --ssl-cert=/etc/mysql/certs/server-cert.pem
50
+ - --ssl-key=/etc/mysql/certs/server-key.pem
51
+ - --bind-address=0.0.0.0
52
+ - --require_secure_transport=ON
53
+ ports:
54
+ - "3306:3306"
55
+ volumes:
56
+ - mysql_data:/var/lib/mysql
57
+ - ./certs:/etc/mysql/certs:ro
58
+ healthcheck:
59
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
60
+ timeout: 5s
61
+ retries: 10
62
+ start_period: 30s
63
+
64
+ volumes:
65
+ mysql_data:
package/jest.config.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ testMatch: ['**/test/integration/**/*.test.js'],
4
+ testTimeout: 30000,
5
+ collectCoverage: false,
6
+ verbose: true
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/database-lib",
3
- "version": "11.2.0",
3
+ "version": "11.3.0",
4
4
  "description": "Shared database code for central services",
5
5
  "main": "src/index.js",
6
6
  "license": "Apache-2.0",
@@ -32,10 +32,11 @@
32
32
  "test:unit": "npx tape 'test/unit/**/*.test.js'",
33
33
  "test:unit:spec": "npm run test:unit | tap-spec",
34
34
  "test:xunit": "npm run test:unit | tap-xunit > ./test/results/xunit.xml",
35
+ "test:int": "npx jest test/integration/database.test.js --testTimeout=30000",
35
36
  "test:coverage": "npx nyc --reporter=lcov --reporter=text-summary tapes -- 'test/unit/**/**.test.js'",
36
37
  "test:coverage-check": "npm run test:coverage && nyc check-coverage",
37
38
  "test:functional": "echo 'No functional tests defined'",
38
- "test:integration": "echo 'No integration tests defined'",
39
+ "test:integration": "./test/scripts/test-integration.sh",
39
40
  "audit:fix": "npm audit fix",
40
41
  "audit:check": "npx audit-ci --config ./audit-ci.jsonc",
41
42
  "dep:check": "npx ncu -e 2",
@@ -47,7 +48,8 @@
47
48
  "async-exit-hook": "^2.0.1",
48
49
  "knex": "3.1.0",
49
50
  "lodash": "4.17.21",
50
- "mysql": "2.18.1"
51
+ "mysql": "^2.18.1",
52
+ "mysql2": "^3.14.2"
51
53
  },
52
54
  "overrides": {
53
55
  "tar": "6.2.1",
@@ -68,16 +70,18 @@
68
70
  },
69
71
  "cross-spawn": "7.0.6",
70
72
  "trim": "0.0.3",
73
+ "undici": "6.21.2",
71
74
  "yargs-parser": "21.1.1"
72
75
  },
73
76
  "devDependencies": {
74
- "@mojaloop/sdk-standard-components": "19.14.0",
77
+ "@mojaloop/sdk-standard-components": "19.16.0",
75
78
  "audit-ci": "^7.1.0",
79
+ "jest": "^30.0.4",
76
80
  "npm-check-updates": "18.0.1",
77
81
  "nyc": "17.1.0",
78
82
  "pre-commit": "1.2.2",
79
83
  "proxyquire": "2.1.3",
80
- "sinon": "20.0.0",
84
+ "sinon": "21.0.0",
81
85
  "standard": "17.1.2",
82
86
  "standard-version": "9.5.0",
83
87
  "tap-spec": "^5.0.0",
package/src/database.js CHANGED
@@ -35,6 +35,9 @@ class Database {
35
35
  mysql: (knex) => {
36
36
  return knex('information_schema.tables').where('TABLE_SCHEMA', this._schema).select('TABLE_NAME').then(rows => rows.map(r => r.TABLE_NAME))
37
37
  },
38
+ mysql2: (knex) => {
39
+ return knex('information_schema.tables').where('TABLE_SCHEMA', this._schema).select('TABLE_NAME').then(rows => rows.map(r => r.TABLE_NAME))
40
+ },
38
41
  pg: (knex) => {
39
42
  return knex('pg_catalog.pg_tables').where({ schemaname: 'public' }).select('tablename').then(rows => rows.map(r => r.tablename))
40
43
  }
@@ -0,0 +1,151 @@
1
+ /* global describe, beforeEach, afterEach, test, expect */
2
+ const Database = require('../../src/database')
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ describe('Database Integration Tests with SSL', () => {
7
+ let db
8
+
9
+ beforeEach(() => {
10
+ db = new Database()
11
+ })
12
+
13
+ afterEach(async () => {
14
+ if (db && db._knex) {
15
+ await db.disconnect()
16
+ }
17
+ })
18
+
19
+ test('should connect to MySQL with SSL using mysql2 client and perform database operations', async () => {
20
+ // Read the CA certificate for SSL verification
21
+ const caCertPath = path.join(__dirname, '../../certs/ca.pem')
22
+ expect(fs.existsSync(caCertPath)).toBe(true)
23
+
24
+ const caCert = fs.readFileSync(caCertPath)
25
+
26
+ await db.connect({
27
+ client: 'mysql2',
28
+ connection: {
29
+ host: 'localhost',
30
+ port: 3306,
31
+ user: 'example_user',
32
+ password: 'example_password',
33
+ database: 'example_db',
34
+ multipleStatements: true,
35
+ namedPlaceholders: true,
36
+ ssl: {
37
+ minVersion: 'TLSv1.3',
38
+ rejectUnauthorized: true, // Enable certificate validation
39
+ ca: caCert // Provide CA certificate for validating self-signed cert
40
+ }
41
+ }
42
+ })
43
+
44
+ // Verify connection is established
45
+ expect(db._knex).toBeDefined()
46
+
47
+ // Clean up any existing tables
48
+ await db._knex.schema.dropTableIfExists('accounts')
49
+ await db._knex.schema.dropTableIfExists('users')
50
+
51
+ // Create test tables
52
+ await db._knex.schema.createTable('users', (table) => {
53
+ table.increments('id').primary()
54
+ table.string('name')
55
+ table.string('email')
56
+ })
57
+
58
+ await db._knex.schema.createTable('accounts', (table) => {
59
+ table.increments('id').primary()
60
+ table.integer('user_id').unsigned().references('id').inTable('users')
61
+ table.decimal('balance', 14, 2)
62
+ })
63
+
64
+ // Test data insertion with named placeholders
65
+ await db._knex.raw(
66
+ 'INSERT INTO users (name, email) VALUES (:name, :email)',
67
+ { name: 'Charlie', email: 'charlie@example.com' }
68
+ )
69
+
70
+ // Test multiple statements
71
+ const multiRes = await db._knex.raw('SELECT * FROM users; SELECT * FROM accounts;')
72
+ const [users, accounts] = multiRes[0]
73
+
74
+ expect(users).toHaveLength(1)
75
+ expect(users[0]).toMatchObject({
76
+ id: 1,
77
+ name: 'Charlie',
78
+ email: 'charlie@example.com'
79
+ })
80
+ expect(accounts).toHaveLength(0)
81
+
82
+ // Test streaming functionality
83
+ const streamResults = []
84
+ const stream = db._knex('users').select('*').stream()
85
+ for await (const row of stream) {
86
+ streamResults.push(row)
87
+ }
88
+
89
+ expect(streamResults).toHaveLength(1)
90
+ expect(streamResults[0]).toMatchObject({
91
+ id: 1,
92
+ name: 'Charlie',
93
+ email: 'charlie@example.com'
94
+ })
95
+
96
+ // Test server connection and time query
97
+ const result = await db._knex.raw('SELECT NOW() as currentTime;')
98
+ expect(result[0][0].currentTime).toBeDefined()
99
+ expect(result[0][0].currentTime).toBeInstanceOf(Date)
100
+
101
+ // Clean up test tables
102
+ await db._knex.schema.dropTableIfExists('accounts')
103
+ await db._knex.schema.dropTableIfExists('users')
104
+ })
105
+
106
+ test('should handle mysql client authentication incompatibility with MySQL 8.0', async () => {
107
+ // Read the CA certificate for SSL verification
108
+ const caCertPath = path.join(__dirname, '../../certs/ca.pem')
109
+ expect(fs.existsSync(caCertPath)).toBe(true)
110
+
111
+ const caCert = fs.readFileSync(caCertPath)
112
+
113
+ // The legacy mysql client doesn't support MySQL 8.0's default caching_sha2_password
114
+ await expect(db.connect({
115
+ client: 'mysql',
116
+ connection: {
117
+ host: 'localhost',
118
+ port: 3306,
119
+ user: 'example_user',
120
+ password: 'example_password',
121
+ database: 'example_db',
122
+ multipleStatements: true,
123
+ ssl: {
124
+ minVersion: 'TLSv1.3',
125
+ rejectUnauthorized: true,
126
+ ca: caCert
127
+ }
128
+ }
129
+ })).rejects.toThrow('Client does not support authentication protocol requested by server')
130
+ })
131
+
132
+ test('should fail when CA certificate is missing (mysql2)', async () => {
133
+ const invalidCaCert = Buffer.from('invalid-certificate')
134
+
135
+ await expect(db.connect({
136
+ client: 'mysql2',
137
+ connection: {
138
+ host: 'localhost',
139
+ port: 3306,
140
+ user: 'example_user',
141
+ password: 'example_password',
142
+ database: 'example_db',
143
+ ssl: {
144
+ minVersion: 'TLSv1.3',
145
+ rejectUnauthorized: true,
146
+ ca: invalidCaCert
147
+ }
148
+ }
149
+ })).rejects.toThrow()
150
+ })
151
+ })
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+
3
+ # Start docker compose in detached mode
4
+ docker compose up -d
5
+
6
+ # Wait for MySQL to be ready
7
+ echo "Waiting for MySQL to be ready..."
8
+ RETRIES=30
9
+ until docker compose exec -T mysql mysqladmin ping -h"127.0.0.1" --silent; do
10
+ RETRIES=$((RETRIES-1))
11
+ if [ $RETRIES -le 0 ]; then
12
+ echo "MySQL did not become ready in time."
13
+ echo "Checking MySQL logs:"
14
+ docker compose logs mysql
15
+ echo "Checking certificate files:"
16
+ docker compose exec -T mysql ls -la /etc/mysql/certs/ || echo "Could not access certs directory"
17
+ exit 1
18
+ fi
19
+ sleep 2
20
+ done
21
+
22
+ echo "MySQL is ready. Running integration tests..."
23
+
24
+ # Verify SSL is enabled
25
+ echo "Verifying SSL configuration..."
26
+ docker compose exec -T mysql mysql -u example_user -pexample_password -e "SHOW VARIABLES LIKE 'have_ssl';" example_db
27
+
28
+ npm run test:int
29
+
30
+ EXIT_CODE=$?
31
+
32
+ # Tear down docker compose
33
+ docker compose down -v
34
+
35
+ exit $EXIT_CODE
@@ -420,5 +420,56 @@ Test('database', databaseTest => {
420
420
  connectTest.end()
421
421
  })
422
422
 
423
+ databaseTest.test('listTableQueries should', listTableQueriesTest => {
424
+ listTableQueriesTest.test('list tables for mysql2 client', async test => {
425
+ // Arrange
426
+ const mysql2TableNames = [{ TABLE_NAME: 'products' }, { TABLE_NAME: 'orders' }]
427
+ // Patch knexConnStub to simulate mysql2
428
+ knexConnStub.client = { config: { client: 'mysql2' } }
429
+ // Patch the stub to return mysql2TableNames for mysql2
430
+ knexConnStub.withArgs('information_schema.tables').returns({
431
+ where: sandbox.stub().withArgs('TABLE_SCHEMA', 'databaseSchema').returns({
432
+ select: sandbox.stub().withArgs('TABLE_NAME').returns(Promise.resolve(mysql2TableNames))
433
+ })
434
+ })
435
+
436
+ // Patch knexStub to return the patched knexConnStub
437
+ knexStub.returns(knexConnStub)
438
+
439
+ // Act
440
+ await dbInstance.connect({ ...connectionConfig, client: 'mysql2' })
441
+
442
+ // Assert
443
+ test.ok(knexStub.calledOnce)
444
+ test.equal(knexStub.firstCall.args[0].client, 'mysql2')
445
+ test.equal(dbInstance._tables.length, mysql2TableNames.length)
446
+ mysql2TableNames.forEach(tbl => {
447
+ test.ok(dbInstance[tbl.TABLE_NAME])
448
+ })
449
+ test.end()
450
+ })
451
+
452
+ listTableQueriesTest.test('throw error for unsupported db type', async test => {
453
+ // Arrange
454
+ const unsupportedClient = 'sqlite3'
455
+ knexConnStub.client = { config: { client: unsupportedClient } }
456
+ knexStub.returns(knexConnStub)
457
+
458
+ // Remove sqlite3 from _listTableQueries if present
459
+ dbInstance._listTableQueries[unsupportedClient] = undefined
460
+
461
+ // Act & Assert
462
+ try {
463
+ await dbInstance.connect({ ...connectionConfig, client: unsupportedClient })
464
+ test.fail('Should have thrown error')
465
+ } catch (err) {
466
+ test.equal(err.message, `Listing tables is not supported for database type ${unsupportedClient}`)
467
+ }
468
+ test.end()
469
+ })
470
+
471
+ listTableQueriesTest.end()
472
+ })
473
+
423
474
  databaseTest.end()
424
475
  })