tina4-nodejs 3.13.92 → 3.13.95
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/CLAUDE.md +170 -28
- package/README.md +2 -2
- package/package.json +13 -9
- package/packages/cli/dist/bin.js +33126 -30055
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +33062 -29908
- package/packages/core/src/ai.ts +7 -1
- package/packages/core/src/auth.ts +191 -39
- package/packages/core/src/background.ts +19 -19
- package/packages/core/src/cache.ts +492 -49
- package/packages/core/src/devAdmin.ts +79 -32
- package/packages/core/src/devMailbox.ts +20 -44
- package/packages/core/src/dispatchPipeline.ts +285 -0
- package/packages/core/src/dotenv.ts +185 -40
- package/packages/core/src/index.ts +7 -6
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +81 -13
- package/packages/core/src/metrics.ts +199 -961
- package/packages/core/src/middleware.ts +390 -123
- package/packages/core/src/queue.ts +188 -32
- package/packages/core/src/queueBackends/kafkaBackend.ts +109 -13
- package/packages/core/src/queueBackends/liteBackend.ts +13 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
- package/packages/core/src/rateLimiter.ts +10 -5
- package/packages/core/src/request.ts +6 -9
- package/packages/core/src/response.ts +46 -1
- package/packages/core/src/router.ts +29 -4
- package/packages/core/src/server.ts +751 -414
- package/packages/core/src/session.ts +244 -27
- package/packages/core/src/sessionHandlers/childError.ts +72 -0
- package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +293 -202
- package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
- package/packages/core/src/sessionHandlers/respClient.ts +16 -143
- package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
- package/packages/core/src/testClient.ts +18 -5
- package/packages/core/src/trustedProxy.ts +249 -0
- package/packages/core/src/types.ts +29 -5
- package/packages/core/src/websocket.ts +66 -0
- package/packages/frond/dist/index.js +74 -31
- package/packages/frond/src/engine.ts +99 -33
- package/packages/orm/dist/index.js +26554 -23400
- package/packages/orm/src/adapters/firebird.ts +183 -56
- package/packages/orm/src/adapters/mongodb.ts +25 -4
- package/packages/orm/src/adapters/mssql.ts +114 -29
- package/packages/orm/src/adapters/mysql.ts +103 -40
- package/packages/orm/src/adapters/odbc.ts +44 -21
- package/packages/orm/src/adapters/postgres.ts +118 -26
- package/packages/orm/src/adapters/sqlDialect.ts +120 -0
- package/packages/orm/src/adapters/sqlite.ts +64 -25
- package/packages/orm/src/baseModel.ts +135 -40
- package/packages/orm/src/cachedDatabase.ts +43 -19
- package/packages/orm/src/connectTimeout.ts +265 -0
- package/packages/orm/src/database.ts +338 -198
- package/packages/orm/src/databaseResult.ts +65 -13
- package/packages/orm/src/databaseUrl.ts +484 -0
- package/packages/orm/src/docstore.ts +386 -145
- package/packages/orm/src/index.ts +13 -3
- package/packages/orm/src/migration.ts +18 -3
- package/packages/orm/src/queryBuilder.ts +38 -4
- package/packages/orm/src/sqlTranslator.ts +310 -4
- package/packages/orm/src/types.ts +15 -4
- package/types/cli/src/bin.d.ts +92 -0
- package/types/cli/src/commands/build.d.ts +2 -0
- package/types/cli/src/commands/generate.d.ts +47 -0
- package/types/cli/src/commands/init.d.ts +1 -0
- package/types/cli/src/commands/metrics.d.ts +6 -0
- package/types/cli/src/commands/migrate.d.ts +1 -0
- package/types/cli/src/commands/migrateCreate.d.ts +1 -0
- package/types/cli/src/commands/migrateRollback.d.ts +1 -0
- package/types/cli/src/commands/migrateStatus.d.ts +1 -0
- package/types/cli/src/commands/queue.d.ts +20 -0
- package/types/cli/src/commands/routes.d.ts +1 -0
- package/types/cli/src/commands/seed.d.ts +1 -0
- package/types/cli/src/commands/serve.d.ts +6 -0
- package/types/cli/src/commands/test.d.ts +1 -0
- package/types/core/src/ai.d.ts +64 -0
- package/types/core/src/api.d.ts +262 -0
- package/types/core/src/auth.d.ts +177 -0
- package/types/core/src/authGate.d.ts +20 -0
- package/types/core/src/background.d.ts +34 -0
- package/types/core/src/cache.d.ts +163 -0
- package/types/core/src/constants.d.ts +38 -0
- package/types/core/src/container.d.ts +44 -0
- package/types/core/src/context/chunker.d.ts +31 -0
- package/types/core/src/context/index.d.ts +93 -0
- package/types/core/src/devAdmin.d.ts +179 -0
- package/types/core/src/devMailbox.d.ts +54 -0
- package/types/core/src/dispatchPipeline.d.ts +117 -0
- package/types/core/src/docs.d.ts +141 -0
- package/types/core/src/docsAutoDiscovery.d.ts +6 -0
- package/types/core/src/dotenv.d.ts +87 -0
- package/types/core/src/env.d.ts +28 -0
- package/types/core/src/errorOverlay.d.ts +36 -0
- package/types/core/src/events.d.ts +75 -0
- package/types/core/src/fakeData.d.ts +55 -0
- package/types/core/src/feedback.d.ts +90 -0
- package/types/core/src/graphql.d.ts +207 -0
- package/types/core/src/health.d.ts +22 -0
- package/types/core/src/htmlElement.d.ts +75 -0
- package/types/core/src/i18n.d.ts +37 -0
- package/types/core/src/index.d.ts +92 -0
- package/types/core/src/job.d.ts +39 -0
- package/types/core/src/logger.d.ts +200 -0
- package/types/core/src/mcp.d.ts +248 -0
- package/types/core/src/messenger.d.ts +191 -0
- package/types/core/src/metrics.d.ts +41 -0
- package/types/core/src/middleware.d.ts +330 -0
- package/types/core/src/mqtt.d.ts +257 -0
- package/types/core/src/mqttMessage.d.ts +67 -0
- package/types/core/src/plan.d.ts +96 -0
- package/types/core/src/projectIndex.d.ts +56 -0
- package/types/core/src/queue.d.ts +268 -0
- package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
- package/types/core/src/queueBackends/liteBackend.d.ts +128 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +119 -0
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
- package/types/core/src/rateLimiter.d.ts +49 -0
- package/types/core/src/request.d.ts +25 -0
- package/types/core/src/response.d.ts +28 -0
- package/types/core/src/routeDiscovery.d.ts +12 -0
- package/types/core/src/router.d.ts +366 -0
- package/types/core/src/scss.d.ts +19 -0
- package/types/core/src/server.d.ts +146 -0
- package/types/core/src/service.d.ts +115 -0
- package/types/core/src/session.d.ts +341 -0
- package/types/core/src/sessionHandlers/childError.d.ts +34 -0
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +97 -0
- package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
- package/types/core/src/sessionHandlers/mongoClient.d.ts +35 -0
- package/types/core/src/sessionHandlers/mongoHandler.d.ts +109 -0
- package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
- package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
- package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
- package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
- package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
- package/types/core/src/static.d.ts +2 -0
- package/types/core/src/test.d.ts +94 -0
- package/types/core/src/testClient.d.ts +36 -0
- package/types/core/src/testing.d.ts +58 -0
- package/types/core/src/trustedProxy.d.ts +44 -0
- package/types/core/src/types.d.ts +242 -0
- package/types/core/src/validator.d.ts +52 -0
- package/types/core/src/websocket.d.ts +402 -0
- package/types/core/src/websocketBackplane.d.ts +166 -0
- package/types/core/src/websocketConnection.d.ts +54 -0
- package/types/core/src/wsdl.d.ts +101 -0
- package/types/frond/src/engine.d.ts +263 -0
- package/types/frond/src/index.d.ts +2 -0
- package/types/orm/src/adapters/firebird.d.ts +183 -0
- package/types/orm/src/adapters/mongodb.d.ts +81 -0
- package/types/orm/src/adapters/mssql.d.ts +77 -0
- package/types/orm/src/adapters/mysql.d.ts +67 -0
- package/types/orm/src/adapters/odbc.d.ts +94 -0
- package/types/orm/src/adapters/postgres.d.ts +86 -0
- package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
- package/types/orm/src/adapters/sqlite.d.ts +68 -0
- package/types/orm/src/autoCrud.d.ts +73 -0
- package/types/orm/src/baseModel.d.ts +427 -0
- package/types/orm/src/cachedDatabase.d.ts +190 -0
- package/types/orm/src/connectTimeout.d.ts +100 -0
- package/types/orm/src/database.d.ts +655 -0
- package/types/orm/src/databaseResult.d.ts +109 -0
- package/types/orm/src/databaseUrl.d.ts +125 -0
- package/types/orm/src/docstore.d.ts +241 -0
- package/types/orm/src/fakeData.d.ts +22 -0
- package/types/orm/src/index.d.ts +43 -0
- package/types/orm/src/migration.d.ts +275 -0
- package/types/orm/src/model.d.ts +7 -0
- package/types/orm/src/query.d.ts +14 -0
- package/types/orm/src/queryBuilder.d.ts +193 -0
- package/types/orm/src/realtime/index.d.ts +7 -0
- package/types/orm/src/realtime/models/attachment.d.ts +43 -0
- package/types/orm/src/realtime/models/channel.d.ts +32 -0
- package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
- package/types/orm/src/realtime/models/message.d.ts +36 -0
- package/types/orm/src/realtime/models/workspace.d.ts +26 -0
- package/types/orm/src/realtime/realtime.d.ts +24 -0
- package/types/orm/src/realtime/storage.d.ts +61 -0
- package/types/orm/src/seeder.d.ts +118 -0
- package/types/orm/src/sqlTranslator.d.ts +258 -0
- package/types/orm/src/types.d.ts +148 -0
- package/types/orm/src/validation.d.ts +6 -0
- package/types/swagger/src/generator.d.ts +46 -0
- package/types/swagger/src/index.d.ts +2 -0
- package/types/swagger/src/ui.d.ts +11 -0
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -206
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Install: npm install mysql2
|
|
5
5
|
* URL format: mysql://user:pass@host:port/database
|
|
6
6
|
*/
|
|
7
|
+
import { MYSQL_DIALECT, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
|
|
7
8
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
9
|
import { SQLTranslator } from "../sqlTranslator.js";
|
|
10
|
+
import { connectTarget, connectTimeoutMillis, driverConnectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
|
|
9
11
|
import { createRequire } from "node:module";
|
|
10
12
|
|
|
11
13
|
let mysql2: any = null;
|
|
@@ -37,6 +39,14 @@ export interface MysqlConfig {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export class MysqlAdapter implements DatabaseAdapter {
|
|
42
|
+
/**
|
|
43
|
+
* Postgres, MySQL and MSSQL all REQUIRE a name for a derived table, so
|
|
44
|
+
* the COUNT probe in Database.countProbe wraps as
|
|
45
|
+
* `FROM (sql) AS _count_query`. SQLite and Firebird leave this unset and
|
|
46
|
+
* get no alias - Firebird rejects `AS` in that position.
|
|
47
|
+
*/
|
|
48
|
+
readonly countSubqueryAlias = "_count_query";
|
|
49
|
+
|
|
40
50
|
private connection: any = null;
|
|
41
51
|
private _lastInsertId: number | bigint | null = null;
|
|
42
52
|
// True between startTransactionAsync() and commit/rollback. executeManyAsync
|
|
@@ -50,33 +60,55 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
50
60
|
async connect(): Promise<void> {
|
|
51
61
|
const mod = requireMysql2();
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
else
|
|
78
|
-
|
|
79
|
-
|
|
63
|
+
// mysql2 has its own connectTimeout (10s by default). It is set from the
|
|
64
|
+
// Tina4 budget so ONE variable governs, and omitted when the bound is
|
|
65
|
+
// disabled so the driver keeps exactly the behaviour it had before.
|
|
66
|
+
const budgetMs = connectTimeoutMillis();
|
|
67
|
+
const driverMs = driverConnectTimeoutMillis(budgetMs);
|
|
68
|
+
const timeoutOption = driverMs === null ? {} : { connectTimeout: driverMs };
|
|
69
|
+
|
|
70
|
+
const { host, port } = connectTarget(this.config, 3306);
|
|
71
|
+
// createConnection() IS the arming point - mysql2 starts its connectTimeout
|
|
72
|
+
// at the END OF ITS CONSTRUCTOR (base/connection.js), not inside connect() -
|
|
73
|
+
// so it has to sit inside the thunk for the Tina4 clock to start first.
|
|
74
|
+
await withConnectTimeout(
|
|
75
|
+
() => {
|
|
76
|
+
if (typeof this.config === "string") {
|
|
77
|
+
// Parse URL: mysql://user:pass@host:port/database
|
|
78
|
+
const url = new URL(this.config);
|
|
79
|
+
this.connection = mod.createConnection({
|
|
80
|
+
host: url.hostname || "localhost",
|
|
81
|
+
port: url.port ? parseInt(url.port, 10) : 3306,
|
|
82
|
+
user: decodeURIComponent(url.username),
|
|
83
|
+
password: decodeURIComponent(url.password),
|
|
84
|
+
database: url.pathname.replace(/^\//, ""),
|
|
85
|
+
...timeoutOption,
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
this.connection = mod.createConnection({
|
|
89
|
+
host: this.config.host ?? "localhost",
|
|
90
|
+
port: this.config.port ?? 3306,
|
|
91
|
+
user: this.config.user,
|
|
92
|
+
password: this.config.password,
|
|
93
|
+
database: this.config.database,
|
|
94
|
+
...timeoutOption,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Promisify the connection
|
|
99
|
+
return new Promise<void>((resolve, reject) => {
|
|
100
|
+
this.connection.connect((err: Error | null) => {
|
|
101
|
+
if (err) reject(err);
|
|
102
|
+
else resolve();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
budgetMs,
|
|
107
|
+
host,
|
|
108
|
+
port,
|
|
109
|
+
// Answered after we gave up: destroy it so the socket does not outlive the boot.
|
|
110
|
+
() => { try { this.connection?.destroy?.(); } catch { /* already gone */ } },
|
|
111
|
+
);
|
|
80
112
|
}
|
|
81
113
|
|
|
82
114
|
private ensureConnected(): void {
|
|
@@ -195,8 +227,7 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
195
227
|
if (Array.isArray(data)) {
|
|
196
228
|
if (data.length === 0) return { success: true, affectedRows: 0 };
|
|
197
229
|
const keys = Object.keys(data[0]);
|
|
198
|
-
const
|
|
199
|
-
const sql = `INSERT INTO \`${table}\` (\`${keys.join("`, `")}\`) VALUES (${placeholders})`;
|
|
230
|
+
const sql = buildInsert(MYSQL_DIALECT, table, keys);
|
|
200
231
|
const paramsList = data.map((row) => keys.map((k) => row[k]));
|
|
201
232
|
try {
|
|
202
233
|
const result = await this.executeManyAsync(sql, paramsList);
|
|
@@ -208,8 +239,7 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
208
239
|
}
|
|
209
240
|
|
|
210
241
|
const keys = Object.keys(data);
|
|
211
|
-
const
|
|
212
|
-
const sql = `INSERT INTO \`${table}\` (\`${keys.join("`, `")}\`) VALUES (${placeholders})`;
|
|
242
|
+
const sql = buildInsert(MYSQL_DIALECT, table, keys);
|
|
213
243
|
const values = Object.values(data);
|
|
214
244
|
|
|
215
245
|
try {
|
|
@@ -229,11 +259,29 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
229
259
|
throw new Error("Use updateAsync() for MySQL.");
|
|
230
260
|
}
|
|
231
261
|
|
|
232
|
-
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
262
|
+
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
|
|
233
263
|
this.ensureConnected();
|
|
234
|
-
const setClauses = Object.keys(data)
|
|
235
|
-
|
|
236
|
-
|
|
264
|
+
const setClauses = buildSetClause(MYSQL_DIALECT, Object.keys(data));
|
|
265
|
+
|
|
266
|
+
// A raw WHERE fragment + params is half the write_path contract's filter
|
|
267
|
+
// form. Without this branch Object.keys("id = ?") yields the STRING INDICES
|
|
268
|
+
// ["0","1",...], producing `WHERE \`0\` = ? AND \`1\` = ?` — MySQL then
|
|
269
|
+
// reports an unknown column '0'. MySQL already uses `?`, so the fragment
|
|
270
|
+
// needs no placeholder rewriting.
|
|
271
|
+
if (typeof filter === "string") {
|
|
272
|
+
const where = filter ? ` WHERE ${filter}` : "";
|
|
273
|
+
const sql = `UPDATE ${MYSQL_DIALECT.quote(table)} SET ${setClauses}${where}`;
|
|
274
|
+
const values = [...Object.values(data), ...(params ?? [])];
|
|
275
|
+
try {
|
|
276
|
+
const result = await this.queryPromise(sql, values);
|
|
277
|
+
return { success: true, affectedRows: result.affectedRows ?? 0 };
|
|
278
|
+
} catch (e) {
|
|
279
|
+
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const whereClauses = buildWhereClause(MYSQL_DIALECT, Object.keys(filter));
|
|
284
|
+
const sql = `UPDATE ${MYSQL_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereClauses}`;
|
|
237
285
|
const values = [...Object.values(data), ...Object.values(filter)];
|
|
238
286
|
|
|
239
287
|
try {
|
|
@@ -248,10 +296,25 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
248
296
|
throw new Error("Use deleteAsync() for MySQL.");
|
|
249
297
|
}
|
|
250
298
|
|
|
251
|
-
async deleteAsync(table: string, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
299
|
+
async deleteAsync(table: string, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
|
|
252
300
|
this.ensureConnected();
|
|
253
|
-
|
|
254
|
-
|
|
301
|
+
|
|
302
|
+
// See updateAsync: truncate() calls this with "1 = 1", which became
|
|
303
|
+
// `WHERE \`0\` = ? AND \`1\` = ? ...` — db.truncate() was broken outright.
|
|
304
|
+
if (typeof filter === "string") {
|
|
305
|
+
const sql = filter
|
|
306
|
+
? `DELETE FROM \`${table}\` WHERE ${filter}`
|
|
307
|
+
: `DELETE FROM \`${table}\``;
|
|
308
|
+
try {
|
|
309
|
+
const result = await this.queryPromise(sql, params ?? []);
|
|
310
|
+
return { success: true, affectedRows: result.affectedRows ?? 0 };
|
|
311
|
+
} catch (e) {
|
|
312
|
+
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const whereClauses = buildWhereClause(MYSQL_DIALECT, Object.keys(filter));
|
|
317
|
+
const sql = `DELETE FROM ${MYSQL_DIALECT.quote(table)} WHERE ${whereClauses}`;
|
|
255
318
|
const values = Object.values(filter);
|
|
256
319
|
|
|
257
320
|
try {
|
|
@@ -289,7 +352,7 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
289
352
|
this._inTransaction = false;
|
|
290
353
|
}
|
|
291
354
|
|
|
292
|
-
|
|
355
|
+
getTables(): string[] {
|
|
293
356
|
throw new Error("Use tablesAsync() for MySQL.");
|
|
294
357
|
}
|
|
295
358
|
|
|
@@ -298,7 +361,7 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
298
361
|
return rows.map((r) => Object.values(r)[0]);
|
|
299
362
|
}
|
|
300
363
|
|
|
301
|
-
|
|
364
|
+
getColumns(table: string): ColumnInfo[] {
|
|
302
365
|
throw new Error("Use columnsAsync() for MySQL.");
|
|
303
366
|
}
|
|
304
367
|
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* The connection string after stripping the "odbc:///" prefix is passed
|
|
9
9
|
* directly to odbc.connect(), so any valid ODBC connection string works.
|
|
10
10
|
*/
|
|
11
|
+
import { ANSI_DIALECT, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
|
|
11
12
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
12
13
|
import { createRequire } from "node:module";
|
|
14
|
+
import { SQLTranslator } from "../sqlTranslator.js";
|
|
15
|
+
import { connectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
|
|
13
16
|
|
|
14
17
|
let odbcModule: any = null;
|
|
15
18
|
|
|
@@ -53,6 +56,21 @@ export class OdbcAdapter implements DatabaseAdapter {
|
|
|
53
56
|
return this.config.connectionString;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The address for a diagnostic message. ODBC hides it inside an opaque
|
|
61
|
+
* driver keyword string, so this reads the standard keywords and falls back to
|
|
62
|
+
* the data-source name - it is never used to connect, only to say which target
|
|
63
|
+
* hung.
|
|
64
|
+
*/
|
|
65
|
+
private describeTarget(): { host: string; port: number | string } {
|
|
66
|
+
const connectionString = this.getConnectionString();
|
|
67
|
+
const host = /(?:^|;)\s*(?:SERVER|SERVERNAME|HOST)\s*=\s*([^;]+)/i.exec(connectionString)?.[1]?.trim()
|
|
68
|
+
?? /(?:^|;)\s*DSN\s*=\s*([^;]+)/i.exec(connectionString)?.[1]?.trim()
|
|
69
|
+
?? "the ODBC data source";
|
|
70
|
+
const port = /(?:^|;)\s*PORT\s*=\s*(\d+)/i.exec(connectionString)?.[1] ?? "unspecified";
|
|
71
|
+
return { host, port };
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
/** Connect to the ODBC data source. Must be called before using the adapter. */
|
|
57
75
|
async connect(): Promise<void> {
|
|
58
76
|
const odbc = requireOdbc();
|
|
@@ -62,7 +80,19 @@ export class OdbcAdapter implements DatabaseAdapter {
|
|
|
62
80
|
if (!connectFn) {
|
|
63
81
|
throw new Error("odbc module does not export a connect() function. Check your odbc package version.");
|
|
64
82
|
}
|
|
65
|
-
|
|
83
|
+
// Outer bound only: the connection string is the driver's, and injecting a
|
|
84
|
+
// CONNECTIONTIMEOUT keyword into a string the operator wrote would be this
|
|
85
|
+
// adapter editing configuration it does not own. The bound frees the CALLER;
|
|
86
|
+
// the native driver thread it is waiting on cannot be cancelled from JS.
|
|
87
|
+
const { host, port } = this.describeTarget();
|
|
88
|
+
this.connection = await withConnectTimeout(
|
|
89
|
+
() => connectFn(connStr),
|
|
90
|
+
connectTimeoutMillis(),
|
|
91
|
+
host,
|
|
92
|
+
port,
|
|
93
|
+
// Answered after we gave up: close it so the handle does not outlive the boot.
|
|
94
|
+
(arrived: any) => { try { void arrived?.close?.(); } catch { /* already gone */ } },
|
|
95
|
+
);
|
|
66
96
|
}
|
|
67
97
|
|
|
68
98
|
private ensureConnected(): void {
|
|
@@ -119,11 +149,11 @@ export class OdbcAdapter implements DatabaseAdapter {
|
|
|
119
149
|
throw new Error("Use rollbackAsync() for ODBC — async adapter requires async methods.");
|
|
120
150
|
}
|
|
121
151
|
|
|
122
|
-
|
|
152
|
+
getTables(): string[] {
|
|
123
153
|
throw new Error("Use tablesAsync() for ODBC — async adapter requires async methods.");
|
|
124
154
|
}
|
|
125
155
|
|
|
126
|
-
|
|
156
|
+
getColumns(table: string): ColumnInfo[] {
|
|
127
157
|
throw new Error("Use columnsAsync() for ODBC — async adapter requires async methods.");
|
|
128
158
|
}
|
|
129
159
|
|
|
@@ -195,16 +225,10 @@ export class OdbcAdapter implements DatabaseAdapter {
|
|
|
195
225
|
limit?: number,
|
|
196
226
|
skip?: number,
|
|
197
227
|
): Promise<T[]> {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
effectiveSql += ` LIMIT ${limit}`;
|
|
203
|
-
if (skip !== undefined && skip > 0) {
|
|
204
|
-
effectiveSql += ` OFFSET ${skip}`;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
228
|
+
// See SQLTranslator.appendLimit: the old inline check was a substring search,
|
|
229
|
+
// so a LIMIT in a string literal or a trailing comment silently dropped the
|
|
230
|
+
// row cap and returned every row.
|
|
231
|
+
const effectiveSql = SQLTranslator.appendLimit(sql, limit, skip);
|
|
208
232
|
return this.queryAsync<T>(effectiveSql, params);
|
|
209
233
|
}
|
|
210
234
|
|
|
@@ -218,8 +242,7 @@ export class OdbcAdapter implements DatabaseAdapter {
|
|
|
218
242
|
async insertAsync(table: string, data: Record<string, unknown>): Promise<DatabaseResult> {
|
|
219
243
|
this.ensureConnected();
|
|
220
244
|
const keys = Object.keys(data);
|
|
221
|
-
const
|
|
222
|
-
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
|
|
245
|
+
const sql = buildInsert(ANSI_DIALECT, table, keys);
|
|
223
246
|
const values = Object.values(data);
|
|
224
247
|
|
|
225
248
|
try {
|
|
@@ -233,9 +256,9 @@ export class OdbcAdapter implements DatabaseAdapter {
|
|
|
233
256
|
/** Update rows in a table matching filter. */
|
|
234
257
|
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
235
258
|
this.ensureConnected();
|
|
236
|
-
const setClauses = Object.keys(data)
|
|
237
|
-
const whereClauses = Object.keys(filter)
|
|
238
|
-
const sql = `UPDATE
|
|
259
|
+
const setClauses = buildSetClause(ANSI_DIALECT, Object.keys(data));
|
|
260
|
+
const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
|
|
261
|
+
const sql = `UPDATE ${ANSI_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereClauses}`;
|
|
239
262
|
const values = [...Object.values(data), ...Object.values(filter)];
|
|
240
263
|
|
|
241
264
|
try {
|
|
@@ -274,8 +297,8 @@ export class OdbcAdapter implements DatabaseAdapter {
|
|
|
274
297
|
}
|
|
275
298
|
}
|
|
276
299
|
|
|
277
|
-
const whereClauses = Object.keys(filter)
|
|
278
|
-
const sql = `DELETE FROM
|
|
300
|
+
const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
|
|
301
|
+
const sql = `DELETE FROM ${ANSI_DIALECT.quote(table)} WHERE ${whereClauses}`;
|
|
279
302
|
const values = Object.values(filter);
|
|
280
303
|
|
|
281
304
|
try {
|
|
@@ -326,7 +349,7 @@ export class OdbcAdapter implements DatabaseAdapter {
|
|
|
326
349
|
/** Get column metadata for a table using ODBC catalog functions. */
|
|
327
350
|
async columnsAsync(table: string): Promise<ColumnInfo[]> {
|
|
328
351
|
this.ensureConnected();
|
|
329
|
-
// odbc.Connection.
|
|
352
|
+
// odbc.Connection.getColumns(catalog, schema, table, column)
|
|
330
353
|
const rows: any[] = await this.connection.columns(null, null, table, null);
|
|
331
354
|
return rows.map((r: any) => ({
|
|
332
355
|
name: r.COLUMN_NAME ?? r.column_name,
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Install: npm install pg @types/pg
|
|
5
5
|
* URL format: postgresql://user:pass@host:port/database
|
|
6
6
|
*/
|
|
7
|
+
import { ANSI_DIALECT, POSTGRES_DIALECT, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
|
|
7
8
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
9
|
import { SQLTranslator } from "../sqlTranslator.js";
|
|
10
|
+
import { connectTarget, connectTimeoutMillis, driverConnectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
|
|
9
11
|
|
|
10
12
|
import { createRequire } from "node:module";
|
|
11
13
|
|
|
@@ -67,6 +69,14 @@ export interface PostgresConfig {
|
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
export class PostgresAdapter implements DatabaseAdapter {
|
|
72
|
+
/**
|
|
73
|
+
* Postgres, MySQL and MSSQL all REQUIRE a name for a derived table, so
|
|
74
|
+
* the COUNT probe in Database.countProbe wraps as
|
|
75
|
+
* `FROM (sql) AS _count_query`. SQLite and Firebird leave this unset and
|
|
76
|
+
* get no alias - Firebird rejects `AS` in that position.
|
|
77
|
+
*/
|
|
78
|
+
readonly countSubqueryAlias = "_count_query";
|
|
79
|
+
|
|
70
80
|
private client: InstanceType<typeof import("pg").Client> | null = null;
|
|
71
81
|
// string is included for non-integer primary keys: a UUID PK (the common
|
|
72
82
|
// `id uuid PRIMARY KEY DEFAULT gen_random_uuid()` shape) returns its id as a
|
|
@@ -84,13 +94,31 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
84
94
|
const pgModule = requirePg();
|
|
85
95
|
const Client = pgModule.Client ?? (pgModule as any).default?.Client;
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
// pg's own connectionTimeoutMillis defaults to 0 - no timeout at all - so
|
|
98
|
+
// without this a server that accepts and never completes the startup
|
|
99
|
+
// handshake hangs the boot forever. Omitted entirely when the bound is
|
|
100
|
+
// disabled, which restores exactly the old (unbounded) behaviour.
|
|
101
|
+
const budgetMs = connectTimeoutMillis();
|
|
102
|
+
const driverMs = driverConnectTimeoutMillis(budgetMs);
|
|
103
|
+
const timeoutOption = driverMs === null ? {} : { connectionTimeoutMillis: driverMs };
|
|
104
|
+
|
|
105
|
+
const { host, port } = connectTarget(this.config, 5432);
|
|
106
|
+
// Everything that touches pg lives inside the thunk so the Tina4 clock
|
|
107
|
+
// starts before pg arms its own timer (client.js `_connect`, cleared only at
|
|
108
|
+
// ReadyForQuery - so the knob covers the whole handshake, not just the TCP).
|
|
109
|
+
await withConnectTimeout(
|
|
110
|
+
() => {
|
|
111
|
+
this.client = typeof this.config === "string"
|
|
112
|
+
? new Client({ connectionString: this.config, ...timeoutOption })
|
|
113
|
+
: new Client({ ...this.config, ...timeoutOption });
|
|
114
|
+
return this.client!.connect();
|
|
115
|
+
},
|
|
116
|
+
budgetMs,
|
|
117
|
+
host,
|
|
118
|
+
port,
|
|
119
|
+
// Answered after we gave up: end it so the socket does not outlive the boot.
|
|
120
|
+
() => { void Promise.resolve(this.client?.end()).catch(() => { /* already gone */ }); },
|
|
121
|
+
);
|
|
94
122
|
}
|
|
95
123
|
|
|
96
124
|
// NOTE: a plain runtime guard, not an `asserts this is ...` predicate. A
|
|
@@ -112,8 +140,11 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
112
140
|
return row;
|
|
113
141
|
}
|
|
114
142
|
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
// `startAt` lets a caller that has already consumed N placeholders (an UPDATE
|
|
144
|
+
// whose SET values are $1..$N) continue the numbering into a raw WHERE
|
|
145
|
+
// fragment instead of restarting at $1.
|
|
146
|
+
private convertPlaceholders(sql: string, startAt = 1): string {
|
|
147
|
+
let count = startAt - 1;
|
|
117
148
|
return sql.replace(/\?/g, () => {
|
|
118
149
|
count++;
|
|
119
150
|
return `$${count}`;
|
|
@@ -244,8 +275,8 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
244
275
|
if (Array.isArray(data)) {
|
|
245
276
|
if (data.length === 0) return { success: true, affectedRows: 0 };
|
|
246
277
|
const keys = Object.keys(data[0]);
|
|
247
|
-
|
|
248
|
-
const sql =
|
|
278
|
+
// The batch path binds through executeManyAsync, which converts "?" itself.
|
|
279
|
+
const sql = buildInsert(ANSI_DIALECT, table, keys);
|
|
249
280
|
const paramsList = data.map((row) => keys.map((k) => row[k]));
|
|
250
281
|
try {
|
|
251
282
|
const result = await this.executeManyAsync(sql, paramsList);
|
|
@@ -257,8 +288,7 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
257
288
|
}
|
|
258
289
|
|
|
259
290
|
const keys = Object.keys(data);
|
|
260
|
-
const
|
|
261
|
-
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders}) RETURNING *`;
|
|
291
|
+
const sql = buildInsert(POSTGRES_DIALECT, table, keys, " RETURNING *");
|
|
262
292
|
const values = Object.values(data);
|
|
263
293
|
|
|
264
294
|
try {
|
|
@@ -280,15 +310,35 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
280
310
|
throw new Error("Use updateAsync() for PostgreSQL.");
|
|
281
311
|
}
|
|
282
312
|
|
|
283
|
-
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
313
|
+
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
|
|
284
314
|
this.ensureConnected();
|
|
285
315
|
const dataKeys = Object.keys(data);
|
|
286
|
-
const filterKeys = Object.keys(filter);
|
|
287
316
|
let paramIndex = 1;
|
|
317
|
+
const setClauses = buildSetClause(POSTGRES_DIALECT, dataKeys, paramIndex);
|
|
318
|
+
paramIndex += dataKeys.length;
|
|
319
|
+
|
|
320
|
+
// A raw WHERE fragment + params is half the write_path contract's filter
|
|
321
|
+
// form ("a string filter with params works the same as a hash filter").
|
|
322
|
+
// Without this branch Object.keys("id = ?") yields the STRING INDICES
|
|
323
|
+
// ["0","1",...], producing `WHERE "0" = $2 AND "1" = $3` — the engine then
|
|
324
|
+
// reports `column "0" does not exist`. sqlite/mongodb/odbc already carried
|
|
325
|
+
// this branch; postgres/mysql/mssql/firebird did not.
|
|
326
|
+
if (typeof filter === "string") {
|
|
327
|
+
const where = filter ? ` WHERE ${this.convertPlaceholders(filter, paramIndex)}` : "";
|
|
328
|
+
const sql = `UPDATE ${POSTGRES_DIALECT.quote(table)} SET ${setClauses}${where}`;
|
|
329
|
+
const values = [...Object.values(data), ...(params ?? [])];
|
|
330
|
+
try {
|
|
331
|
+
const result = await this.client!.query(sql, values);
|
|
332
|
+
return { success: true, affectedRows: result.rowCount ?? 0 };
|
|
333
|
+
} catch (e) {
|
|
334
|
+
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
288
337
|
|
|
289
|
-
const
|
|
290
|
-
const whereClauses =
|
|
291
|
-
|
|
338
|
+
const filterKeys = Object.keys(filter);
|
|
339
|
+
const whereClauses = buildWhereClause(POSTGRES_DIALECT, filterKeys, paramIndex);
|
|
340
|
+
paramIndex += filterKeys.length;
|
|
341
|
+
const sql = `UPDATE ${POSTGRES_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereClauses}`;
|
|
292
342
|
const values = [...Object.values(data), ...Object.values(filter)];
|
|
293
343
|
|
|
294
344
|
try {
|
|
@@ -303,12 +353,30 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
303
353
|
throw new Error("Use deleteAsync() for PostgreSQL.");
|
|
304
354
|
}
|
|
305
355
|
|
|
306
|
-
async deleteAsync(table: string, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
356
|
+
async deleteAsync(table: string, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
|
|
307
357
|
this.ensureConnected();
|
|
358
|
+
|
|
359
|
+
// See updateAsync: a raw WHERE fragment must not be walked as an object.
|
|
360
|
+
// truncate() calls this with "1 = 1", which became `WHERE "0" = $1 AND
|
|
361
|
+
// "1" = $2 ...` and failed with `column "0" does not exist` — db.truncate()
|
|
362
|
+
// was broken outright on PostgreSQL.
|
|
363
|
+
if (typeof filter === "string") {
|
|
364
|
+
const sql = filter
|
|
365
|
+
? `DELETE FROM "${table}" WHERE ${this.convertPlaceholders(filter)}`
|
|
366
|
+
: `DELETE FROM "${table}"`;
|
|
367
|
+
try {
|
|
368
|
+
const result = await this.client!.query(sql, params ?? []);
|
|
369
|
+
return { success: true, affectedRows: result.rowCount ?? 0 };
|
|
370
|
+
} catch (e) {
|
|
371
|
+
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
308
375
|
const filterKeys = Object.keys(filter);
|
|
309
376
|
let paramIndex = 1;
|
|
310
|
-
const whereClauses =
|
|
311
|
-
|
|
377
|
+
const whereClauses = buildWhereClause(POSTGRES_DIALECT, filterKeys, paramIndex);
|
|
378
|
+
paramIndex += filterKeys.length;
|
|
379
|
+
const sql = `DELETE FROM ${POSTGRES_DIALECT.quote(table)} WHERE ${whereClauses}`;
|
|
312
380
|
const values = Object.values(filter);
|
|
313
381
|
|
|
314
382
|
try {
|
|
@@ -354,7 +422,7 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
354
422
|
this._inTransaction = false;
|
|
355
423
|
}
|
|
356
424
|
|
|
357
|
-
|
|
425
|
+
getTables(): string[] {
|
|
358
426
|
throw new Error("Use tablesAsync() for PostgreSQL.");
|
|
359
427
|
}
|
|
360
428
|
|
|
@@ -371,28 +439,52 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
371
439
|
);
|
|
372
440
|
}
|
|
373
441
|
|
|
374
|
-
|
|
442
|
+
getColumns(table: string): ColumnInfo[] {
|
|
375
443
|
throw new Error("Use columnsAsync() for PostgreSQL.");
|
|
376
444
|
}
|
|
377
445
|
|
|
378
446
|
async columnsAsync(table: string): Promise<ColumnInfo[]> {
|
|
379
447
|
// v3.13.14 (#48): honour a schema-qualified name; default to public.
|
|
380
448
|
const [schema, tbl] = SQLTranslator.splitSchema(table);
|
|
449
|
+
// primaryKey was hardcoded false, so primaryKey(table) introspected NOTHING
|
|
450
|
+
// on PostgreSQL. The filterless-write guard (feature 4) reads it, so
|
|
451
|
+
// update(table, data) keyed on the primary key in `data` threw "update
|
|
452
|
+
// requires a filter or the complete primary key in the data" against every
|
|
453
|
+
// PostgreSQL table. Port the Python master's LEFT JOIN so the cross-engine
|
|
454
|
+
// columns() contract (#48) actually holds here — the subquery yields every
|
|
455
|
+
// column of the PK, so a COMPOSITE key reports true on each of its columns,
|
|
456
|
+
// not just the first.
|
|
381
457
|
const rows = await this.queryAsync<{
|
|
382
458
|
column_name: string;
|
|
383
459
|
data_type: string;
|
|
384
460
|
is_nullable: string;
|
|
385
461
|
column_default: string | null;
|
|
462
|
+
is_primary: boolean | string;
|
|
386
463
|
}>(
|
|
387
|
-
|
|
388
|
-
|
|
464
|
+
`SELECT c.column_name, c.data_type, c.is_nullable, c.column_default,
|
|
465
|
+
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END AS is_primary
|
|
466
|
+
FROM information_schema.columns c
|
|
467
|
+
LEFT JOIN (
|
|
468
|
+
SELECT ku.column_name
|
|
469
|
+
FROM information_schema.table_constraints tc
|
|
470
|
+
JOIN information_schema.key_column_usage ku
|
|
471
|
+
ON tc.constraint_name = ku.constraint_name
|
|
472
|
+
AND tc.table_schema = ku.table_schema
|
|
473
|
+
WHERE tc.table_name = $1 AND tc.table_schema = $2
|
|
474
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
|
475
|
+
) pk ON c.column_name = pk.column_name
|
|
476
|
+
WHERE c.table_name = $3 AND c.table_schema = $4
|
|
477
|
+
ORDER BY c.ordinal_position`,
|
|
478
|
+
[tbl, schema ?? "public", tbl, schema ?? "public"],
|
|
389
479
|
);
|
|
390
480
|
return rows.map((r) => ({
|
|
391
481
|
name: r.column_name,
|
|
392
482
|
type: r.data_type,
|
|
393
483
|
nullable: r.is_nullable === "YES",
|
|
394
484
|
default: r.column_default,
|
|
395
|
-
|
|
485
|
+
// node-postgres decodes bool to true/false; stay tolerant of the raw
|
|
486
|
+
// "t" text form in case the type parser is bypassed.
|
|
487
|
+
primaryKey: r.is_primary === true || r.is_primary === "t",
|
|
396
488
|
}));
|
|
397
489
|
}
|
|
398
490
|
|