@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.
@@ -0,0 +1,345 @@
1
+ /**
2
+ * MostaJdbcBridge — Universal HTTP-to-JDBC bridge for @mostajs/orm
3
+ *
4
+ * Accepts POST /query with JSON { "sql": "...", "params": [...] }
5
+ * Executes via JDBC and returns JSON results.
6
+ *
7
+ * Works with any JDBC driver: HSQLDB, Oracle, DB2, Sybase, etc.
8
+ *
9
+ * Usage:
10
+ * java -cp "MostaJdbcBridge.java:hsqldb-2.7.2.jar" MostaJdbcBridge \
11
+ * --jdbc-url jdbc:hsqldb:hsql://localhost:9001/xdb \
12
+ * --user SA --password "" --port 8765
13
+ *
14
+ * Compile-free (Java 11+ source launcher):
15
+ * java --source 11 -cp hsqldb-2.7.2.jar MostaJdbcBridge.java \
16
+ * --jdbc-url jdbc:hsqldb:hsql://localhost:9001/xdb
17
+ *
18
+ * Author: Dr Hamid MADANI drmdh@msn.com
19
+ */
20
+
21
+ import com.sun.net.httpserver.HttpServer;
22
+ import com.sun.net.httpserver.HttpExchange;
23
+
24
+ import java.io.*;
25
+ import java.net.InetSocketAddress;
26
+ import java.nio.charset.StandardCharsets;
27
+ import java.sql.*;
28
+ import java.util.*;
29
+ import java.util.concurrent.Executors;
30
+
31
+ public class MostaJdbcBridge {
32
+
33
+ private static String jdbcUrl;
34
+ private static String dbUser = "SA";
35
+ private static String dbPassword = "";
36
+ private static int httpPort = 8765;
37
+ private static Connection connection;
38
+
39
+ public static void main(String[] args) throws Exception {
40
+ parseArgs(args);
41
+
42
+ System.out.println("[MostaJdbcBridge] Connecting to: " + jdbcUrl);
43
+ connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPassword);
44
+ System.out.println("[MostaJdbcBridge] JDBC connected OK");
45
+
46
+ HttpServer server = HttpServer.create(new InetSocketAddress(httpPort), 0);
47
+ server.setExecutor(Executors.newFixedThreadPool(4));
48
+
49
+ server.createContext("/query", MostaJdbcBridge::handleQuery);
50
+ server.createContext("/health", MostaJdbcBridge::handleHealth);
51
+
52
+ server.start();
53
+ System.out.println("[MostaJdbcBridge] HTTP bridge listening on port " + httpPort);
54
+ System.out.println("[MostaJdbcBridge] POST /query — execute SQL");
55
+ System.out.println("[MostaJdbcBridge] GET /health — health check");
56
+ }
57
+
58
+ // ── /query handler ──────────────────────────────────────────
59
+
60
+ private static void handleQuery(HttpExchange ex) throws IOException {
61
+ // CORS headers
62
+ ex.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
63
+ ex.getResponseHeaders().add("Access-Control-Allow-Methods", "POST, OPTIONS");
64
+ ex.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type");
65
+
66
+ if ("OPTIONS".equalsIgnoreCase(ex.getRequestMethod())) {
67
+ ex.sendResponseHeaders(204, -1);
68
+ return;
69
+ }
70
+
71
+ if (!"POST".equalsIgnoreCase(ex.getRequestMethod())) {
72
+ sendJson(ex, 405, "{\"error\":\"Method not allowed\"}");
73
+ return;
74
+ }
75
+
76
+ try {
77
+ String body = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
78
+ Map<String, Object> request = parseJson(body);
79
+
80
+ String sql = (String) request.get("sql");
81
+ List<?> params = (List<?>) request.getOrDefault("params", Collections.emptyList());
82
+
83
+ if (sql == null || sql.isBlank()) {
84
+ sendJson(ex, 400, "{\"error\":\"Missing 'sql' field\"}");
85
+ return;
86
+ }
87
+
88
+ // Reconnect if connection was lost
89
+ if (connection == null || connection.isClosed()) {
90
+ connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPassword);
91
+ }
92
+
93
+ String sqlUpper = sql.trim().toUpperCase();
94
+ boolean isQuery = sqlUpper.startsWith("SELECT")
95
+ || sqlUpper.startsWith("SHOW")
96
+ || sqlUpper.startsWith("DESCRIBE")
97
+ || sqlUpper.startsWith("EXPLAIN");
98
+
99
+ if (isQuery) {
100
+ handleSelect(ex, sql, params);
101
+ } else {
102
+ handleUpdate(ex, sql, params);
103
+ }
104
+
105
+ } catch (Exception e) {
106
+ String msg = e.getMessage() != null ? e.getMessage() : e.getClass().getName();
107
+ sendJson(ex, 500, "{\"error\":" + jsonString(msg) + "}");
108
+ }
109
+ }
110
+
111
+ // ── SELECT → JSON array of objects ──────────────────────────
112
+
113
+ private static void handleSelect(HttpExchange ex, String sql, List<?> params) throws Exception {
114
+ try (PreparedStatement ps = connection.prepareStatement(sql)) {
115
+ bindParams(ps, params);
116
+ try (ResultSet rs = ps.executeQuery()) {
117
+ ResultSetMetaData meta = rs.getMetaData();
118
+ int colCount = meta.getColumnCount();
119
+
120
+ StringBuilder sb = new StringBuilder("[");
121
+ boolean first = true;
122
+ while (rs.next()) {
123
+ if (!first) sb.append(",");
124
+ first = false;
125
+ sb.append("{");
126
+ for (int i = 1; i <= colCount; i++) {
127
+ if (i > 1) sb.append(",");
128
+ String colName = meta.getColumnLabel(i);
129
+ Object value = rs.getObject(i);
130
+ sb.append(jsonString(colName)).append(":");
131
+ sb.append(jsonValue(value));
132
+ }
133
+ sb.append("}");
134
+ }
135
+ sb.append("]");
136
+ sendJson(ex, 200, sb.toString());
137
+ }
138
+ }
139
+ }
140
+
141
+ // ── INSERT/UPDATE/DELETE → { changes: N } ───────────────────
142
+
143
+ private static void handleUpdate(HttpExchange ex, String sql, List<?> params) throws Exception {
144
+ try (PreparedStatement ps = connection.prepareStatement(sql)) {
145
+ bindParams(ps, params);
146
+ int changes = ps.executeUpdate();
147
+ sendJson(ex, 200, "{\"changes\":" + changes + "}");
148
+ }
149
+ }
150
+
151
+ // ── /health handler ─────────────────────────────────────────
152
+
153
+ private static void handleHealth(HttpExchange ex) throws IOException {
154
+ ex.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
155
+ boolean ok = false;
156
+ try {
157
+ ok = connection != null && !connection.isClosed() && connection.isValid(2);
158
+ } catch (SQLException ignored) {}
159
+ String json = "{\"status\":" + (ok ? "\"ok\"" : "\"error\"") + ",\"jdbcUrl\":" + jsonString(jdbcUrl) + "}";
160
+ sendJson(ex, ok ? 200 : 503, json);
161
+ }
162
+
163
+ // ── Helpers ─────────────────────────────────────────────────
164
+
165
+ private static void bindParams(PreparedStatement ps, List<?> params) throws SQLException {
166
+ if (params == null) return;
167
+ for (int i = 0; i < params.size(); i++) {
168
+ Object v = params.get(i);
169
+ if (v == null) ps.setNull(i + 1, Types.NULL);
170
+ else if (v instanceof String) ps.setString(i + 1, (String) v);
171
+ else if (v instanceof Integer) ps.setInt(i + 1, (Integer) v);
172
+ else if (v instanceof Long) ps.setLong(i + 1, (Long) v);
173
+ else if (v instanceof Double) ps.setDouble(i + 1, (Double) v);
174
+ else if (v instanceof Float) ps.setFloat(i + 1, (Float) v);
175
+ else if (v instanceof Boolean) ps.setBoolean(i + 1, (Boolean) v);
176
+ else ps.setString(i + 1, v.toString());
177
+ }
178
+ }
179
+
180
+ private static void sendJson(HttpExchange ex, int status, String json) throws IOException {
181
+ byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
182
+ ex.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
183
+ ex.sendResponseHeaders(status, bytes.length);
184
+ try (OutputStream os = ex.getResponseBody()) {
185
+ os.write(bytes);
186
+ }
187
+ }
188
+
189
+ private static String jsonString(String s) {
190
+ if (s == null) return "null";
191
+ return "\"" + s.replace("\\", "\\\\")
192
+ .replace("\"", "\\\"")
193
+ .replace("\n", "\\n")
194
+ .replace("\r", "\\r")
195
+ .replace("\t", "\\t") + "\"";
196
+ }
197
+
198
+ private static String jsonValue(Object v) {
199
+ if (v == null) return "null";
200
+ if (v instanceof Number) return v.toString();
201
+ if (v instanceof Boolean) return v.toString();
202
+ return jsonString(v.toString());
203
+ }
204
+
205
+ /** Minimal JSON parser for { "sql": "...", "params": [...] } */
206
+ @SuppressWarnings("unchecked")
207
+ private static Map<String, Object> parseJson(String json) {
208
+ json = json.trim();
209
+ Map<String, Object> map = new HashMap<>();
210
+ if (!json.startsWith("{")) return map;
211
+
212
+ // Remove outer braces
213
+ json = json.substring(1, json.lastIndexOf('}'));
214
+
215
+ // Find "sql" value
216
+ int sqlKeyIdx = json.indexOf("\"sql\"");
217
+ if (sqlKeyIdx >= 0) {
218
+ int colonIdx = json.indexOf(':', sqlKeyIdx + 4);
219
+ String sqlVal = extractStringValue(json, colonIdx + 1);
220
+ map.put("sql", sqlVal);
221
+ }
222
+
223
+ // Find "params" array
224
+ int paramsKeyIdx = json.indexOf("\"params\"");
225
+ if (paramsKeyIdx >= 0) {
226
+ int bracketStart = json.indexOf('[', paramsKeyIdx);
227
+ int bracketEnd = findMatchingBracket(json, bracketStart);
228
+ if (bracketStart >= 0 && bracketEnd >= 0) {
229
+ String arrStr = json.substring(bracketStart + 1, bracketEnd).trim();
230
+ List<Object> params = parseArray(arrStr);
231
+ map.put("params", params);
232
+ }
233
+ }
234
+
235
+ return map;
236
+ }
237
+
238
+ private static String extractStringValue(String json, int fromIdx) {
239
+ int firstQuote = json.indexOf('"', fromIdx);
240
+ if (firstQuote < 0) return "";
241
+ StringBuilder sb = new StringBuilder();
242
+ boolean escaped = false;
243
+ for (int i = firstQuote + 1; i < json.length(); i++) {
244
+ char c = json.charAt(i);
245
+ if (escaped) {
246
+ switch (c) {
247
+ case 'n': sb.append('\n'); break;
248
+ case 'r': sb.append('\r'); break;
249
+ case 't': sb.append('\t'); break;
250
+ default: sb.append(c); break;
251
+ }
252
+ escaped = false;
253
+ } else if (c == '\\') {
254
+ escaped = true;
255
+ } else if (c == '"') {
256
+ break;
257
+ } else {
258
+ sb.append(c);
259
+ }
260
+ }
261
+ return sb.toString();
262
+ }
263
+
264
+ private static int findMatchingBracket(String s, int openIdx) {
265
+ if (openIdx < 0) return -1;
266
+ int depth = 0;
267
+ boolean inString = false;
268
+ boolean escaped = false;
269
+ for (int i = openIdx; i < s.length(); i++) {
270
+ char c = s.charAt(i);
271
+ if (escaped) { escaped = false; continue; }
272
+ if (c == '\\') { escaped = true; continue; }
273
+ if (c == '"') { inString = !inString; continue; }
274
+ if (inString) continue;
275
+ if (c == '[') depth++;
276
+ if (c == ']') { depth--; if (depth == 0) return i; }
277
+ }
278
+ return -1;
279
+ }
280
+
281
+ private static List<Object> parseArray(String arrContent) {
282
+ List<Object> list = new ArrayList<>();
283
+ if (arrContent.isEmpty()) return list;
284
+
285
+ int i = 0;
286
+ while (i < arrContent.length()) {
287
+ char c = arrContent.charAt(i);
288
+ if (c == ' ' || c == ',' || c == '\n' || c == '\r' || c == '\t') { i++; continue; }
289
+
290
+ if (c == '"') {
291
+ String val = extractStringValue(arrContent, i - 1);
292
+ list.add(val);
293
+ // Skip past the closing quote
294
+ i++; // skip opening quote
295
+ boolean esc = false;
296
+ while (i < arrContent.length()) {
297
+ char ch = arrContent.charAt(i);
298
+ if (esc) { esc = false; i++; continue; }
299
+ if (ch == '\\') { esc = true; i++; continue; }
300
+ if (ch == '"') { i++; break; }
301
+ i++;
302
+ }
303
+ } else if (c == 'n' && arrContent.startsWith("null", i)) {
304
+ list.add(null);
305
+ i += 4;
306
+ } else if (c == 't' && arrContent.startsWith("true", i)) {
307
+ list.add(Boolean.TRUE);
308
+ i += 4;
309
+ } else if (c == 'f' && arrContent.startsWith("false", i)) {
310
+ list.add(Boolean.FALSE);
311
+ i += 5;
312
+ } else if (c == '-' || Character.isDigit(c)) {
313
+ int start = i;
314
+ boolean isDouble = false;
315
+ while (i < arrContent.length() && (Character.isDigit(arrContent.charAt(i)) || arrContent.charAt(i) == '.' || arrContent.charAt(i) == '-' || arrContent.charAt(i) == 'e' || arrContent.charAt(i) == 'E')) {
316
+ if (arrContent.charAt(i) == '.' || arrContent.charAt(i) == 'e' || arrContent.charAt(i) == 'E') isDouble = true;
317
+ i++;
318
+ }
319
+ String numStr = arrContent.substring(start, i);
320
+ if (isDouble) list.add(Double.parseDouble(numStr));
321
+ else list.add(Long.parseLong(numStr));
322
+ } else {
323
+ i++;
324
+ }
325
+ }
326
+ return list;
327
+ }
328
+
329
+ private static void parseArgs(String[] args) {
330
+ for (int i = 0; i < args.length; i++) {
331
+ switch (args[i]) {
332
+ case "--jdbc-url": case "-j": jdbcUrl = args[++i]; break;
333
+ case "--user": case "-u": dbUser = args[++i]; break;
334
+ case "--password": case "-p": dbPassword = args[++i]; break;
335
+ case "--port": httpPort = Integer.parseInt(args[++i]); break;
336
+ default: break;
337
+ }
338
+ }
339
+ if (jdbcUrl == null || jdbcUrl.isBlank()) {
340
+ System.err.println("Usage: java MostaJdbcBridge --jdbc-url <JDBC_URL> [--user SA] [--password \"\"] [--port 8765]");
341
+ System.err.println("Example: java --source 11 -cp hsqldb-2.7.2.jar MostaJdbcBridge.java --jdbc-url jdbc:hsqldb:hsql://localhost:9001/xdb");
342
+ System.exit(1);
343
+ }
344
+ }
345
+ }
@@ -0,0 +1,74 @@
1
+ import type { DialectType } from '../core/types.js';
2
+ import { JdbcNormalizer } from './JdbcNormalizer.js';
3
+ export interface BridgeInstance {
4
+ /** Unique key: `${dialect}:${host}:${port}/${database}` */
5
+ key: string;
6
+ /** Dialect type */
7
+ dialect: DialectType;
8
+ /** HTTP port of the bridge */
9
+ port: number;
10
+ /** HTTP base URL: http://localhost:{port} */
11
+ url: string;
12
+ /** PID of the Java process */
13
+ pid: number;
14
+ /** JDBC URL used */
15
+ jdbcUrl: string;
16
+ /** When the bridge was started */
17
+ startedAt: Date;
18
+ /** Underlying JdbcNormalizer instance */
19
+ normalizer: JdbcNormalizer;
20
+ }
21
+ export declare class BridgeManager {
22
+ private static instance;
23
+ private bridges;
24
+ private nextPort;
25
+ private readonly basePort;
26
+ private readonly portIncrement;
27
+ private readonly maxRetries;
28
+ private readonly retryResetMs;
29
+ private startAttempts;
30
+ private cleanupRegistered;
31
+ private constructor();
32
+ static getInstance(): BridgeManager;
33
+ /** Reset singleton (for testing) */
34
+ static resetInstance(): void;
35
+ /**
36
+ * Get an existing bridge or create a new one for the given dialect/URI.
37
+ * If a bridge with the same key already exists and is alive, reuse it.
38
+ */
39
+ getOrCreate(dialect: DialectType, uri: string, options?: {
40
+ jarDir?: string;
41
+ bridgeJavaFile?: string;
42
+ }): Promise<BridgeInstance>;
43
+ /**
44
+ * Stop a specific bridge by key.
45
+ */
46
+ stop(key: string): Promise<void>;
47
+ /**
48
+ * Stop ALL bridges (called on app exit).
49
+ */
50
+ stopAll(): Promise<void>;
51
+ /**
52
+ * List all active bridges.
53
+ */
54
+ list(): BridgeInstance[];
55
+ /**
56
+ * Check if a bridge exists for the given key.
57
+ */
58
+ has(key: string): boolean;
59
+ /**
60
+ * Build a bridge key from dialect and URI.
61
+ */
62
+ buildKey(dialect: DialectType, uri: string): string;
63
+ private startBridge;
64
+ private getNextPort;
65
+ private isAlive;
66
+ private detectExistingBridge;
67
+ private checkStartAttempts;
68
+ private getJarDir;
69
+ private writePidFile;
70
+ private removePidFile;
71
+ private cleanupOrphans;
72
+ private registerCleanupHandlers;
73
+ private stopAllSync;
74
+ }