@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 +9 -0
- package/certs/certs_here +0 -0
- package/docker-compose.yaml +65 -0
- package/jest.config.js +7 -0
- package/package.json +9 -5
- package/src/database.js +3 -0
- package/test/integration/database.test.js +151 -0
- package/test/scripts/test-integration.sh +35 -0
- package/test/unit/database.test.js +51 -0
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
|
|
package/certs/certs_here
ADDED
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mojaloop/database-lib",
|
|
3
|
-
"version": "11.
|
|
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": "
|
|
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.
|
|
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": "
|
|
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
|
})
|