@mostajs/orm 1.0.0 → 1.2.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/bridge/MostaJdbcBridge.java +345 -0
- package/dist/bridge/BridgeManager.d.ts +74 -0
- package/dist/bridge/BridgeManager.js +361 -0
- package/dist/bridge/JdbcNormalizer.d.ts +63 -0
- package/dist/bridge/JdbcNormalizer.js +253 -0
- package/dist/bridge/jdbc-registry.d.ts +31 -0
- package/dist/bridge/jdbc-registry.js +67 -0
- package/dist/dialects/abstract-sql.dialect.d.ts +26 -4
- package/dist/dialects/abstract-sql.dialect.js +77 -3
- package/dist/dialects/db2.dialect.js +2 -2
- package/dist/dialects/hana.dialect.js +2 -2
- package/dist/dialects/hsqldb.dialect.js +22 -56
- package/dist/dialects/mariadb.dialect.js +2 -2
- package/dist/dialects/mssql.dialect.d.ts +2 -2
- package/dist/dialects/mssql.dialect.js +2 -2
- package/dist/dialects/mysql.dialect.d.ts +2 -2
- package/dist/dialects/mysql.dialect.js +2 -2
- package/dist/dialects/oracle.dialect.js +2 -2
- package/dist/dialects/postgres.dialect.d.ts +2 -2
- package/dist/dialects/postgres.dialect.js +2 -2
- package/dist/dialects/spanner.dialect.js +2 -2
- package/dist/dialects/sybase.dialect.js +2 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/docs/audit-dialects-vs-hibernate.md +642 -0
- package/docs/jdbc-normalizer-study.md +843 -0
- package/docs/plan-session-factory-multi-bridge.md +1233 -0
- package/jar_files/hsqldb-2.7.2.jar +0 -0
- package/package.json +3 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// BridgeManager — Singleton managing multiple JDBC bridge instances
|
|
2
|
+
// Allows simultaneous connections to different SGBD via JDBC bridges on incrementing ports
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { JdbcNormalizer, parseUri } from './JdbcNormalizer.js';
|
|
7
|
+
import { getJdbcDriverInfo } from './jdbc-registry.js';
|
|
8
|
+
// ============================================================
|
|
9
|
+
// BridgeManager singleton
|
|
10
|
+
// ============================================================
|
|
11
|
+
export class BridgeManager {
|
|
12
|
+
static instance = null;
|
|
13
|
+
bridges = new Map();
|
|
14
|
+
nextPort;
|
|
15
|
+
basePort;
|
|
16
|
+
portIncrement;
|
|
17
|
+
maxRetries;
|
|
18
|
+
retryResetMs;
|
|
19
|
+
startAttempts = new Map();
|
|
20
|
+
cleanupRegistered = false;
|
|
21
|
+
constructor() {
|
|
22
|
+
this.basePort = parseInt(process.env.MOSTA_BRIDGE_PORT_BASE || '8765');
|
|
23
|
+
this.nextPort = this.basePort;
|
|
24
|
+
this.portIncrement = (process.env.MOSTA_BRIDGE_PORT_INCREMENT ?? 'true') !== 'false';
|
|
25
|
+
this.maxRetries = parseInt(process.env.MOSTA_BRIDGE_MAX_RETRIES || '3');
|
|
26
|
+
this.retryResetMs = 60_000;
|
|
27
|
+
this.registerCleanupHandlers();
|
|
28
|
+
this.cleanupOrphans();
|
|
29
|
+
}
|
|
30
|
+
static getInstance() {
|
|
31
|
+
if (!BridgeManager.instance) {
|
|
32
|
+
BridgeManager.instance = new BridgeManager();
|
|
33
|
+
}
|
|
34
|
+
return BridgeManager.instance;
|
|
35
|
+
}
|
|
36
|
+
/** Reset singleton (for testing) */
|
|
37
|
+
static resetInstance() {
|
|
38
|
+
if (BridgeManager.instance) {
|
|
39
|
+
BridgeManager.instance.stopAllSync();
|
|
40
|
+
BridgeManager.instance = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// ============================================================
|
|
44
|
+
// Public API
|
|
45
|
+
// ============================================================
|
|
46
|
+
/**
|
|
47
|
+
* Get an existing bridge or create a new one for the given dialect/URI.
|
|
48
|
+
* If a bridge with the same key already exists and is alive, reuse it.
|
|
49
|
+
*/
|
|
50
|
+
async getOrCreate(dialect, uri, options) {
|
|
51
|
+
const key = this.buildKey(dialect, uri);
|
|
52
|
+
// Check existing bridge
|
|
53
|
+
if (this.bridges.has(key)) {
|
|
54
|
+
const bridge = this.bridges.get(key);
|
|
55
|
+
if (await this.isAlive(bridge)) {
|
|
56
|
+
return bridge;
|
|
57
|
+
}
|
|
58
|
+
// Dead bridge — clean up and re-create
|
|
59
|
+
this.bridges.delete(key);
|
|
60
|
+
this.removePidFile(bridge.port);
|
|
61
|
+
}
|
|
62
|
+
// Anti-loop protection
|
|
63
|
+
this.checkStartAttempts(key);
|
|
64
|
+
// Autostart check
|
|
65
|
+
const autostart = process.env.MOSTA_BRIDGE_AUTOSTART ?? 'true';
|
|
66
|
+
if (autostart === 'false') {
|
|
67
|
+
// Try to detect an existing bridge on the expected port
|
|
68
|
+
const port = this.getNextPort();
|
|
69
|
+
if (await this.detectExistingBridge(port)) {
|
|
70
|
+
const parsed = parseUri(dialect, uri);
|
|
71
|
+
const jdbcUrl = JdbcNormalizer.composeJdbcUrl(dialect, parsed);
|
|
72
|
+
const normalizer = new JdbcNormalizer();
|
|
73
|
+
// Create a "virtual" bridge instance pointing to the manually started bridge
|
|
74
|
+
const bridge = {
|
|
75
|
+
key,
|
|
76
|
+
dialect,
|
|
77
|
+
port,
|
|
78
|
+
url: `http://localhost:${port}`,
|
|
79
|
+
pid: 0, // Unknown — manually started
|
|
80
|
+
jdbcUrl,
|
|
81
|
+
startedAt: new Date(),
|
|
82
|
+
normalizer,
|
|
83
|
+
};
|
|
84
|
+
// Set normalizer as active without starting a process
|
|
85
|
+
normalizer._active = true;
|
|
86
|
+
normalizer.bridgeUrl = bridge.url;
|
|
87
|
+
this.bridges.set(key, bridge);
|
|
88
|
+
console.log(`[BridgeManager] Reusing existing bridge on port ${port} for ${dialect}`);
|
|
89
|
+
return bridge;
|
|
90
|
+
}
|
|
91
|
+
const info = getJdbcDriverInfo(dialect);
|
|
92
|
+
throw new Error(`JDBC bridge disabled (MOSTA_BRIDGE_AUTOSTART=false).\n` +
|
|
93
|
+
`Start the bridge manually:\n` +
|
|
94
|
+
` java --source 11 -cp ${info?.jarPrefix || 'driver'}*.jar \\\n` +
|
|
95
|
+
` MostaJdbcBridge.java \\\n` +
|
|
96
|
+
` --jdbc-url <jdbc-url> \\\n` +
|
|
97
|
+
` --port ${this.basePort}\n` +
|
|
98
|
+
`Or set MOSTA_BRIDGE_AUTOSTART=true in .env`);
|
|
99
|
+
}
|
|
100
|
+
// Check detect mode — reuse existing if alive
|
|
101
|
+
if (autostart === 'detect') {
|
|
102
|
+
const port = this.getNextPort();
|
|
103
|
+
if (await this.detectExistingBridge(port)) {
|
|
104
|
+
const parsed = parseUri(dialect, uri);
|
|
105
|
+
const jdbcUrl = JdbcNormalizer.composeJdbcUrl(dialect, parsed);
|
|
106
|
+
const normalizer = new JdbcNormalizer();
|
|
107
|
+
const bridge = {
|
|
108
|
+
key,
|
|
109
|
+
dialect,
|
|
110
|
+
port,
|
|
111
|
+
url: `http://localhost:${port}`,
|
|
112
|
+
pid: 0,
|
|
113
|
+
jdbcUrl,
|
|
114
|
+
startedAt: new Date(),
|
|
115
|
+
normalizer,
|
|
116
|
+
};
|
|
117
|
+
normalizer._active = true;
|
|
118
|
+
normalizer.bridgeUrl = bridge.url;
|
|
119
|
+
this.bridges.set(key, bridge);
|
|
120
|
+
console.log(`[BridgeManager] Detected existing bridge on port ${port} for ${dialect}`);
|
|
121
|
+
return bridge;
|
|
122
|
+
}
|
|
123
|
+
// Not found — fall through to launch
|
|
124
|
+
}
|
|
125
|
+
// Launch a new bridge
|
|
126
|
+
try {
|
|
127
|
+
const bridge = await this.startBridge(dialect, uri, options);
|
|
128
|
+
this.startAttempts.delete(key); // Success — reset counter
|
|
129
|
+
return bridge;
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
// Increment attempt counter
|
|
133
|
+
const current = this.startAttempts.get(key) || { count: 0, lastAttempt: new Date() };
|
|
134
|
+
current.count++;
|
|
135
|
+
current.lastAttempt = new Date();
|
|
136
|
+
this.startAttempts.set(key, current);
|
|
137
|
+
throw e;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Stop a specific bridge by key.
|
|
142
|
+
*/
|
|
143
|
+
async stop(key) {
|
|
144
|
+
const bridge = this.bridges.get(key);
|
|
145
|
+
if (!bridge)
|
|
146
|
+
return;
|
|
147
|
+
bridge.normalizer.stop();
|
|
148
|
+
this.removePidFile(bridge.port);
|
|
149
|
+
this.bridges.delete(key);
|
|
150
|
+
console.log(`[BridgeManager] Stopped bridge ${key} on port ${bridge.port}`);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Stop ALL bridges (called on app exit).
|
|
154
|
+
*/
|
|
155
|
+
async stopAll() {
|
|
156
|
+
for (const [key, bridge] of this.bridges) {
|
|
157
|
+
try {
|
|
158
|
+
bridge.normalizer.stop();
|
|
159
|
+
this.removePidFile(bridge.port);
|
|
160
|
+
console.log(`[BridgeManager] Stopped bridge ${key} on port ${bridge.port}`);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Best effort
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
this.bridges.clear();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* List all active bridges.
|
|
170
|
+
*/
|
|
171
|
+
list() {
|
|
172
|
+
return Array.from(this.bridges.values());
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Check if a bridge exists for the given key.
|
|
176
|
+
*/
|
|
177
|
+
has(key) {
|
|
178
|
+
return this.bridges.has(key);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Build a bridge key from dialect and URI.
|
|
182
|
+
*/
|
|
183
|
+
buildKey(dialect, uri) {
|
|
184
|
+
const parsed = parseUri(dialect, uri);
|
|
185
|
+
return `${dialect}:${parsed.host}:${parsed.port || ''}/${parsed.database || ''}`;
|
|
186
|
+
}
|
|
187
|
+
// ============================================================
|
|
188
|
+
// Internal
|
|
189
|
+
// ============================================================
|
|
190
|
+
async startBridge(dialect, uri, options) {
|
|
191
|
+
const port = this.getNextPort();
|
|
192
|
+
// Check port availability
|
|
193
|
+
if (await this.detectExistingBridge(port)) {
|
|
194
|
+
if (!this.portIncrement) {
|
|
195
|
+
throw new Error(`Port ${port} already in use.\n` +
|
|
196
|
+
`Set MOSTA_BRIDGE_PORT_INCREMENT=true to auto-increment,\n` +
|
|
197
|
+
`or change MOSTA_BRIDGE_PORT_BASE,\n` +
|
|
198
|
+
`or stop the process using port ${port}: lsof -i :${port}`);
|
|
199
|
+
}
|
|
200
|
+
// Port taken — increment was already handled by getNextPort()
|
|
201
|
+
// but if we detect it's in use, try next
|
|
202
|
+
this.nextPort++;
|
|
203
|
+
return this.startBridge(dialect, uri, options);
|
|
204
|
+
}
|
|
205
|
+
const normalizer = new JdbcNormalizer();
|
|
206
|
+
await normalizer.start(dialect, uri, {
|
|
207
|
+
bridgePort: port,
|
|
208
|
+
jarDir: options?.jarDir,
|
|
209
|
+
bridgeJavaFile: options?.bridgeJavaFile,
|
|
210
|
+
});
|
|
211
|
+
const parsed = parseUri(dialect, uri);
|
|
212
|
+
const jdbcUrl = JdbcNormalizer.composeJdbcUrl(dialect, parsed);
|
|
213
|
+
const pid = normalizer.process?.pid ?? 0;
|
|
214
|
+
const key = this.buildKey(dialect, uri);
|
|
215
|
+
const bridge = {
|
|
216
|
+
key,
|
|
217
|
+
dialect,
|
|
218
|
+
port,
|
|
219
|
+
url: `http://localhost:${port}`,
|
|
220
|
+
pid,
|
|
221
|
+
jdbcUrl,
|
|
222
|
+
startedAt: new Date(),
|
|
223
|
+
normalizer,
|
|
224
|
+
};
|
|
225
|
+
this.bridges.set(key, bridge);
|
|
226
|
+
this.writePidFile(port, pid);
|
|
227
|
+
// Advance port for next bridge
|
|
228
|
+
if (this.portIncrement) {
|
|
229
|
+
this.nextPort = port + 1;
|
|
230
|
+
}
|
|
231
|
+
console.log(`[BridgeManager] Bridge started: ${key}\n` +
|
|
232
|
+
` Port: ${port} | PID: ${pid} | JDBC: ${jdbcUrl}`);
|
|
233
|
+
return bridge;
|
|
234
|
+
}
|
|
235
|
+
getNextPort() {
|
|
236
|
+
return this.nextPort;
|
|
237
|
+
}
|
|
238
|
+
async isAlive(bridge) {
|
|
239
|
+
try {
|
|
240
|
+
const res = await fetch(`${bridge.url}/health`, {
|
|
241
|
+
signal: AbortSignal.timeout(2000),
|
|
242
|
+
});
|
|
243
|
+
return res.ok;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async detectExistingBridge(port) {
|
|
250
|
+
try {
|
|
251
|
+
const res = await fetch(`http://localhost:${port}/health`, {
|
|
252
|
+
signal: AbortSignal.timeout(2000),
|
|
253
|
+
});
|
|
254
|
+
return res.ok;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
checkStartAttempts(key) {
|
|
261
|
+
const attempts = this.startAttempts.get(key);
|
|
262
|
+
if (!attempts)
|
|
263
|
+
return;
|
|
264
|
+
const elapsed = Date.now() - attempts.lastAttempt.getTime();
|
|
265
|
+
if (elapsed < this.retryResetMs && attempts.count >= this.maxRetries) {
|
|
266
|
+
throw new Error(`JDBC bridge for "${key}" failed ${this.maxRetries} times in the last ${this.retryResetMs / 1000}s. Giving up.\n` +
|
|
267
|
+
`Diagnostic:\n` +
|
|
268
|
+
` 1. Java installed? → java --version\n` +
|
|
269
|
+
` 2. JAR valid? → ls jar_files/\n` +
|
|
270
|
+
` 3. SGBD running? → check target port\n` +
|
|
271
|
+
` 4. Firewall? → check bridge port\n` +
|
|
272
|
+
`Check Java installation, JAR file, and SGBD server.`);
|
|
273
|
+
}
|
|
274
|
+
if (elapsed >= this.retryResetMs) {
|
|
275
|
+
this.startAttempts.delete(key); // Reset after cooldown
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// --- PID file management ---
|
|
279
|
+
getJarDir() {
|
|
280
|
+
return process.env.MOSTA_JAR_DIR || join(process.cwd(), 'jar_files');
|
|
281
|
+
}
|
|
282
|
+
writePidFile(port, pid) {
|
|
283
|
+
try {
|
|
284
|
+
const dir = this.getJarDir();
|
|
285
|
+
if (existsSync(dir)) {
|
|
286
|
+
writeFileSync(join(dir, `.bridge-${port}.pid`), String(pid));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Non-critical
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
removePidFile(port) {
|
|
294
|
+
try {
|
|
295
|
+
const pidFile = join(this.getJarDir(), `.bridge-${port}.pid`);
|
|
296
|
+
if (existsSync(pidFile)) {
|
|
297
|
+
unlinkSync(pidFile);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Non-critical
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
cleanupOrphans() {
|
|
305
|
+
try {
|
|
306
|
+
const dir = this.getJarDir();
|
|
307
|
+
if (!existsSync(dir))
|
|
308
|
+
return;
|
|
309
|
+
const pidFiles = readdirSync(dir).filter(f => f.startsWith('.bridge-') && f.endsWith('.pid'));
|
|
310
|
+
for (const file of pidFiles) {
|
|
311
|
+
try {
|
|
312
|
+
const pidStr = readFileSync(join(dir, file), 'utf-8').trim();
|
|
313
|
+
const pid = parseInt(pidStr);
|
|
314
|
+
if (isNaN(pid) || pid <= 0) {
|
|
315
|
+
unlinkSync(join(dir, file));
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
// Check if process is still alive
|
|
319
|
+
try {
|
|
320
|
+
process.kill(pid, 0); // Signal 0 = check existence
|
|
321
|
+
// Process alive — it's an orphan, kill it
|
|
322
|
+
process.kill(pid, 'SIGTERM');
|
|
323
|
+
console.log(`[BridgeManager] Killed orphan bridge process PID ${pid} (${file})`);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// Process doesn't exist — just clean up the PID file
|
|
327
|
+
}
|
|
328
|
+
unlinkSync(join(dir, file));
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// Ignore individual file errors
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Non-critical — orphan cleanup is best-effort
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// --- Cleanup handlers ---
|
|
340
|
+
registerCleanupHandlers() {
|
|
341
|
+
if (this.cleanupRegistered)
|
|
342
|
+
return;
|
|
343
|
+
this.cleanupRegistered = true;
|
|
344
|
+
const cleanup = () => this.stopAllSync();
|
|
345
|
+
process.on('exit', cleanup);
|
|
346
|
+
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
347
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
348
|
+
}
|
|
349
|
+
stopAllSync() {
|
|
350
|
+
for (const [, bridge] of this.bridges) {
|
|
351
|
+
try {
|
|
352
|
+
bridge.normalizer.stop();
|
|
353
|
+
this.removePidFile(bridge.port);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// Best effort
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
this.bridges.clear();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { DialectType } from '../core/types.js';
|
|
2
|
+
export interface JdbcBridgeConfig {
|
|
3
|
+
dialect: DialectType;
|
|
4
|
+
host: string;
|
|
5
|
+
port?: number;
|
|
6
|
+
database: string;
|
|
7
|
+
user?: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
bridgePort?: number;
|
|
10
|
+
jarDir?: string;
|
|
11
|
+
bridgeJavaFile?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parse a SGBD_URI into host/port/database/user/password components.
|
|
15
|
+
* Handles formats like:
|
|
16
|
+
* hsqldb:hsql://localhost:9001/mydb
|
|
17
|
+
* oracle://user:pass@host:1521/service
|
|
18
|
+
* db2://user:pass@host:50000/mydb
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseUri(dialect: DialectType, uri: string): Omit<JdbcBridgeConfig, 'dialect'>;
|
|
21
|
+
export declare class JdbcNormalizer {
|
|
22
|
+
private process;
|
|
23
|
+
private bridgeUrl;
|
|
24
|
+
private _active;
|
|
25
|
+
/**
|
|
26
|
+
* Try to find a JAR for the given dialect.
|
|
27
|
+
* Returns the full path to the JAR, or null if not found.
|
|
28
|
+
*/
|
|
29
|
+
static findJar(dialect: DialectType, jarDir?: string): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Check if a JDBC bridge is available for this dialect (JAR exists).
|
|
32
|
+
*/
|
|
33
|
+
static isAvailable(dialect: DialectType, jarDir?: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Compose the JDBC URL from parsed URI components.
|
|
36
|
+
*/
|
|
37
|
+
static composeJdbcUrl(dialect: DialectType, config: Omit<JdbcBridgeConfig, 'dialect'>): string;
|
|
38
|
+
/**
|
|
39
|
+
* Start the JDBC bridge for a given dialect and URI.
|
|
40
|
+
* Returns the HTTP base URL (e.g. http://localhost:8765).
|
|
41
|
+
*/
|
|
42
|
+
start(dialect: DialectType, uri: string, options?: {
|
|
43
|
+
bridgePort?: number;
|
|
44
|
+
jarDir?: string;
|
|
45
|
+
bridgeJavaFile?: string;
|
|
46
|
+
}): Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Wait for the bridge health endpoint to respond.
|
|
49
|
+
*/
|
|
50
|
+
private waitForReady;
|
|
51
|
+
/**
|
|
52
|
+
* Execute a query via the bridge HTTP endpoint.
|
|
53
|
+
*/
|
|
54
|
+
query<T>(sql: string, params: unknown[]): Promise<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Stop the bridge process.
|
|
57
|
+
*/
|
|
58
|
+
stop(): void;
|
|
59
|
+
/** Whether the bridge is currently active */
|
|
60
|
+
get active(): boolean;
|
|
61
|
+
/** The HTTP base URL of the running bridge */
|
|
62
|
+
get url(): string;
|
|
63
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// JdbcNormalizer — Auto-detects JDBC JARs and manages the MostaJdbcBridge process
|
|
2
|
+
// Sits between AbstractSqlDialect and MostaJdbcBridge.java
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { readdirSync, existsSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { getJdbcDriverInfo } from './jdbc-registry.js';
|
|
9
|
+
// Resolve paths relative to this file
|
|
10
|
+
const __filename_resolved = typeof __filename !== 'undefined'
|
|
11
|
+
? __filename
|
|
12
|
+
: fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname_resolved = dirname(__filename_resolved);
|
|
14
|
+
/** Default directory for JAR files — configurable via env MOSTA_JAR_DIR */
|
|
15
|
+
function getDefaultJarDir() {
|
|
16
|
+
if (process.env.MOSTA_JAR_DIR)
|
|
17
|
+
return process.env.MOSTA_JAR_DIR;
|
|
18
|
+
// Look in mosta-orm/bridge/, then ../jar_files/ (monorepo layout)
|
|
19
|
+
const candidates = [
|
|
20
|
+
join(__dirname_resolved, '..', '..', 'bridge'),
|
|
21
|
+
join(__dirname_resolved, '..', '..', '..', 'jar_files'),
|
|
22
|
+
join(__dirname_resolved, '..', '..', 'jar_files'),
|
|
23
|
+
];
|
|
24
|
+
for (const dir of candidates) {
|
|
25
|
+
if (existsSync(dir))
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
return candidates[0];
|
|
29
|
+
}
|
|
30
|
+
/** Default path to MostaJdbcBridge.java */
|
|
31
|
+
function getDefaultBridgeJavaPath() {
|
|
32
|
+
if (process.env.MOSTA_BRIDGE_JAVA)
|
|
33
|
+
return process.env.MOSTA_BRIDGE_JAVA;
|
|
34
|
+
const candidates = [
|
|
35
|
+
join(__dirname_resolved, '..', '..', 'bridge', 'MostaJdbcBridge.java'),
|
|
36
|
+
join(__dirname_resolved, '..', '..', '..', 'bridge', 'MostaJdbcBridge.java'),
|
|
37
|
+
];
|
|
38
|
+
for (const p of candidates) {
|
|
39
|
+
if (existsSync(p))
|
|
40
|
+
return p;
|
|
41
|
+
}
|
|
42
|
+
return candidates[0];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse a SGBD_URI into host/port/database/user/password components.
|
|
46
|
+
* Handles formats like:
|
|
47
|
+
* hsqldb:hsql://localhost:9001/mydb
|
|
48
|
+
* oracle://user:pass@host:1521/service
|
|
49
|
+
* db2://user:pass@host:50000/mydb
|
|
50
|
+
*/
|
|
51
|
+
export function parseUri(dialect, uri) {
|
|
52
|
+
const info = getJdbcDriverInfo(dialect);
|
|
53
|
+
const defaultPort = info?.defaultPort ?? 9001;
|
|
54
|
+
const defaultUser = info?.defaultUser ?? 'SA';
|
|
55
|
+
const defaultPassword = info?.defaultPassword ?? '';
|
|
56
|
+
// Handle hsqldb:hsql://host:port/db
|
|
57
|
+
const hsqlMatch = uri.match(/^hsqldb:hsql:\/\/([^:/]+)(?::(\d+))?(?:\/(.*))?$/);
|
|
58
|
+
if (hsqlMatch) {
|
|
59
|
+
return {
|
|
60
|
+
host: hsqlMatch[1],
|
|
61
|
+
port: hsqlMatch[2] ? parseInt(hsqlMatch[2]) : defaultPort,
|
|
62
|
+
database: hsqlMatch[3] || '',
|
|
63
|
+
user: defaultUser,
|
|
64
|
+
password: defaultPassword,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Handle dialect://user:pass@host:port/db
|
|
68
|
+
const standardMatch = uri.match(/^\w+:\/\/(?:([^:@]+)(?::([^@]*))?@)?([^:/]+)(?::(\d+))?(?:\/(.*))?$/);
|
|
69
|
+
if (standardMatch) {
|
|
70
|
+
return {
|
|
71
|
+
host: standardMatch[3],
|
|
72
|
+
port: standardMatch[4] ? parseInt(standardMatch[4]) : defaultPort,
|
|
73
|
+
database: standardMatch[5] || '',
|
|
74
|
+
user: standardMatch[1] ? decodeURIComponent(standardMatch[1]) : defaultUser,
|
|
75
|
+
password: standardMatch[2] ? decodeURIComponent(standardMatch[2]) : defaultPassword,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Fallback
|
|
79
|
+
return {
|
|
80
|
+
host: 'localhost',
|
|
81
|
+
port: defaultPort,
|
|
82
|
+
database: '',
|
|
83
|
+
user: defaultUser,
|
|
84
|
+
password: defaultPassword,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// ============================================================
|
|
88
|
+
// JdbcNormalizer class
|
|
89
|
+
// ============================================================
|
|
90
|
+
export class JdbcNormalizer {
|
|
91
|
+
process = null;
|
|
92
|
+
bridgeUrl = '';
|
|
93
|
+
_active = false;
|
|
94
|
+
/**
|
|
95
|
+
* Try to find a JAR for the given dialect.
|
|
96
|
+
* Returns the full path to the JAR, or null if not found.
|
|
97
|
+
*/
|
|
98
|
+
static findJar(dialect, jarDir) {
|
|
99
|
+
const info = getJdbcDriverInfo(dialect);
|
|
100
|
+
if (!info)
|
|
101
|
+
return null;
|
|
102
|
+
const dir = jarDir || getDefaultJarDir();
|
|
103
|
+
if (!existsSync(dir))
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
const files = readdirSync(dir)
|
|
107
|
+
.filter(f => f.startsWith(info.jarPrefix) && f.endsWith('.jar'))
|
|
108
|
+
.sort();
|
|
109
|
+
if (files.length === 0)
|
|
110
|
+
return null;
|
|
111
|
+
// Return the last (highest version) JAR
|
|
112
|
+
return join(dir, files[files.length - 1]);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Check if a JDBC bridge is available for this dialect (JAR exists).
|
|
120
|
+
*/
|
|
121
|
+
static isAvailable(dialect, jarDir) {
|
|
122
|
+
return JdbcNormalizer.findJar(dialect, jarDir) !== null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Compose the JDBC URL from parsed URI components.
|
|
126
|
+
*/
|
|
127
|
+
static composeJdbcUrl(dialect, config) {
|
|
128
|
+
const info = getJdbcDriverInfo(dialect);
|
|
129
|
+
if (!info)
|
|
130
|
+
throw new Error(`No JDBC registry entry for dialect "${dialect}"`);
|
|
131
|
+
const port = config.port || info.defaultPort;
|
|
132
|
+
return info.jdbcUrlTemplate
|
|
133
|
+
.replace('{host}', config.host)
|
|
134
|
+
.replace('{port}', String(port))
|
|
135
|
+
.replace('{db}', config.database);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Start the JDBC bridge for a given dialect and URI.
|
|
139
|
+
* Returns the HTTP base URL (e.g. http://localhost:8765).
|
|
140
|
+
*/
|
|
141
|
+
async start(dialect, uri, options) {
|
|
142
|
+
const jarDir = options?.jarDir || getDefaultJarDir();
|
|
143
|
+
const jarPath = JdbcNormalizer.findJar(dialect, jarDir);
|
|
144
|
+
if (!jarPath) {
|
|
145
|
+
const info = getJdbcDriverInfo(dialect);
|
|
146
|
+
throw new Error(`No JDBC JAR found for ${info?.label ?? dialect}.\n` +
|
|
147
|
+
`Expected: ${info?.jarPrefix}*.jar in ${jarDir}\n` +
|
|
148
|
+
`Download the JDBC driver and place it in the jar_files/ directory.`);
|
|
149
|
+
}
|
|
150
|
+
const parsed = parseUri(dialect, uri);
|
|
151
|
+
const jdbcUrl = JdbcNormalizer.composeJdbcUrl(dialect, parsed);
|
|
152
|
+
const bridgePort = options?.bridgePort || parseInt(process.env.MOSTA_BRIDGE_PORT || '8765');
|
|
153
|
+
const user = parsed.user || getJdbcDriverInfo(dialect).defaultUser;
|
|
154
|
+
const password = parsed.password ?? getJdbcDriverInfo(dialect).defaultPassword;
|
|
155
|
+
const bridgeJava = options?.bridgeJavaFile || getDefaultBridgeJavaPath();
|
|
156
|
+
if (!existsSync(bridgeJava)) {
|
|
157
|
+
throw new Error(`MostaJdbcBridge.java not found at ${bridgeJava}.\n` +
|
|
158
|
+
`Set MOSTA_BRIDGE_JAVA env or provide bridgeJavaFile option.`);
|
|
159
|
+
}
|
|
160
|
+
// Build classpath: JAR + bridge directory (for any companion JARs)
|
|
161
|
+
const classpath = jarPath;
|
|
162
|
+
this.process = spawn('java', [
|
|
163
|
+
'--source', '11',
|
|
164
|
+
'-cp', classpath,
|
|
165
|
+
bridgeJava,
|
|
166
|
+
'--jdbc-url', jdbcUrl,
|
|
167
|
+
'--user', user,
|
|
168
|
+
'--password', password,
|
|
169
|
+
'--port', String(bridgePort),
|
|
170
|
+
], {
|
|
171
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
172
|
+
});
|
|
173
|
+
// Log bridge stderr for debugging
|
|
174
|
+
this.process.stderr?.on('data', (data) => {
|
|
175
|
+
const msg = data.toString().trim();
|
|
176
|
+
if (msg)
|
|
177
|
+
console.error(`[JdbcBridge:${dialect}] ${msg}`);
|
|
178
|
+
});
|
|
179
|
+
this.process.on('exit', (code) => {
|
|
180
|
+
if (this._active) {
|
|
181
|
+
console.error(`[JdbcBridge:${dialect}] Process exited with code ${code}`);
|
|
182
|
+
this._active = false;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
this.bridgeUrl = `http://localhost:${bridgePort}`;
|
|
186
|
+
// Wait for bridge to be ready
|
|
187
|
+
await this.waitForReady(bridgePort);
|
|
188
|
+
this._active = true;
|
|
189
|
+
console.log(`[JdbcNormalizer] Bridge started for ${dialect} → ${jdbcUrl} on port ${bridgePort}`);
|
|
190
|
+
return this.bridgeUrl;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Wait for the bridge health endpoint to respond.
|
|
194
|
+
*/
|
|
195
|
+
async waitForReady(port, timeoutMs = 15000) {
|
|
196
|
+
const start = Date.now();
|
|
197
|
+
let lastError = '';
|
|
198
|
+
while (Date.now() - start < timeoutMs) {
|
|
199
|
+
try {
|
|
200
|
+
const res = await fetch(`http://localhost:${port}/health`);
|
|
201
|
+
if (res.ok)
|
|
202
|
+
return;
|
|
203
|
+
lastError = `HTTP ${res.status}`;
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
207
|
+
}
|
|
208
|
+
await new Promise(r => setTimeout(r, 300));
|
|
209
|
+
}
|
|
210
|
+
// Kill the process if it didn't start
|
|
211
|
+
this.stop();
|
|
212
|
+
throw new Error(`JDBC bridge not ready after ${timeoutMs}ms on port ${port}.\n` +
|
|
213
|
+
`Last error: ${lastError}\n` +
|
|
214
|
+
`Ensure Java 11+ is installed: java --version`);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Execute a query via the bridge HTTP endpoint.
|
|
218
|
+
*/
|
|
219
|
+
async query(sql, params) {
|
|
220
|
+
if (!this._active) {
|
|
221
|
+
throw new Error('JDBC bridge is not active. Call start() first.');
|
|
222
|
+
}
|
|
223
|
+
const response = await fetch(`${this.bridgeUrl}/query`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify({ sql, params }),
|
|
227
|
+
});
|
|
228
|
+
if (!response.ok) {
|
|
229
|
+
const text = await response.text();
|
|
230
|
+
throw new Error(`JDBC bridge query failed (${response.status}): ${text}`);
|
|
231
|
+
}
|
|
232
|
+
return response.json();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Stop the bridge process.
|
|
236
|
+
*/
|
|
237
|
+
stop() {
|
|
238
|
+
this._active = false;
|
|
239
|
+
if (this.process) {
|
|
240
|
+
this.process.kill('SIGTERM');
|
|
241
|
+
this.process = null;
|
|
242
|
+
}
|
|
243
|
+
this.bridgeUrl = '';
|
|
244
|
+
}
|
|
245
|
+
/** Whether the bridge is currently active */
|
|
246
|
+
get active() {
|
|
247
|
+
return this._active;
|
|
248
|
+
}
|
|
249
|
+
/** The HTTP base URL of the running bridge */
|
|
250
|
+
get url() {
|
|
251
|
+
return this.bridgeUrl;
|
|
252
|
+
}
|
|
253
|
+
}
|