tina4-nodejs 3.13.43 → 3.13.45
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 +2 -1
- package/package.json +1 -1
- package/packages/core/src/graphql.ts +23 -14
- package/packages/core/src/mcp.ts +21 -2
- package/packages/core/src/queueBackends/kafkaBackend.ts +303 -167
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +97 -31
- package/packages/core/src/server.ts +12 -5
- package/packages/core/src/session.ts +11 -95
- package/packages/core/src/sessionHandlers/mongoClient.ts +238 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +25 -204
- package/packages/core/src/sessionHandlers/redisHandler.ts +69 -114
- package/packages/core/src/sessionHandlers/respClient.ts +171 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +11 -95
- package/packages/orm/src/adapters/firebird.ts +20 -2
- package/packages/orm/src/adapters/mssql.ts +24 -2
- package/packages/orm/src/adapters/mysql.ts +20 -2
- package/packages/orm/src/adapters/postgres.ts +40 -12
- package/packages/orm/src/adapters/sqlite.ts +16 -2
- package/packages/orm/src/autoCrud.ts +13 -0
- package/packages/orm/src/baseModel.ts +3 -1
- package/packages/orm/src/cachedDatabase.ts +1 -1
- package/packages/orm/src/database.ts +42 -11
- package/packages/orm/src/index.ts +1 -1
- package/packages/orm/src/migration.ts +124 -45
- package/packages/orm/src/types.ts +5 -3
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* TINA4_SESSION_VALKEY_PREFIX (default: "tina4:session:")
|
|
12
12
|
* TINA4_SESSION_VALKEY_DB (default: 0)
|
|
13
13
|
*/
|
|
14
|
-
import { execFileSync } from "node:child_process";
|
|
15
14
|
import type { SessionHandler } from "../session.js";
|
|
15
|
+
import { respCommandSync } from "./respClient.js";
|
|
16
16
|
|
|
17
17
|
interface SessionData {
|
|
18
18
|
_created: number;
|
|
@@ -75,103 +75,19 @@ export class ValkeySessionHandler implements SessionHandler {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
* Execute a RESP command synchronously
|
|
78
|
+
* Execute a RESP command synchronously against the live Valkey server.
|
|
79
79
|
*
|
|
80
|
-
*
|
|
81
|
-
* transport/connection FAILURE (server unreachable,
|
|
82
|
-
* THROWS so the Session boundary can distinguish "not found"
|
|
83
|
-
* "backend failed" (log-loud + degrade). Backend-failure
|
|
80
|
+
* Delegates to the shared {@link respCommandSync} transport: a genuine key miss
|
|
81
|
+
* yields `""`, and a transport/connection FAILURE (server unreachable, rejected
|
|
82
|
+
* AUTH, timeout) THROWS so the Session boundary can distinguish "not found"
|
|
83
|
+
* (silent) from "backend failed" (log-loud + degrade). Backend-failure parity.
|
|
84
84
|
*/
|
|
85
85
|
private execSync(args: string[]): string {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const db = ${this.db};
|
|
92
|
-
const args = ${JSON.stringify(args)};
|
|
93
|
-
|
|
94
|
-
function buildCommand(a) {
|
|
95
|
-
let cmd = "*" + a.length + "\\r\\n";
|
|
96
|
-
for (const s of a) cmd += "$" + Buffer.byteLength(s) + "\\r\\n" + s + "\\r\\n";
|
|
97
|
-
return cmd;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function parseResp(buf) {
|
|
101
|
-
const str = buf.toString("utf-8");
|
|
102
|
-
if (str.startsWith("+")) return str.slice(1).split("\\r\\n")[0];
|
|
103
|
-
if (str.startsWith("-")) return "ERR:" + str.slice(1).split("\\r\\n")[0];
|
|
104
|
-
if (str.startsWith(":")) return str.slice(1).split("\\r\\n")[0];
|
|
105
|
-
if (str.startsWith("$-1")) return null;
|
|
106
|
-
if (str.startsWith("$")) {
|
|
107
|
-
const nl = str.indexOf("\\r\\n");
|
|
108
|
-
const len = parseInt(str.slice(1, nl), 10);
|
|
109
|
-
const start = nl + 2;
|
|
110
|
-
return str.slice(start, start + len);
|
|
111
|
-
}
|
|
112
|
-
return str;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const sock = net.createConnection({ host, port }, () => {
|
|
116
|
-
let commands = "";
|
|
117
|
-
if (password) commands += buildCommand(["AUTH", password]);
|
|
118
|
-
if (db !== 0) commands += buildCommand(["SELECT", String(db)]);
|
|
119
|
-
commands += buildCommand(args);
|
|
120
|
-
sock.write(commands);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
let buffer = Buffer.alloc(0);
|
|
124
|
-
sock.on("data", (chunk) => {
|
|
125
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
126
|
-
});
|
|
127
|
-
sock.on("end", () => {
|
|
128
|
-
// Parse last response (skip AUTH/SELECT responses)
|
|
129
|
-
const lines = buffer.toString("utf-8").split("\\r\\n");
|
|
130
|
-
let responses = [];
|
|
131
|
-
let i = 0;
|
|
132
|
-
while (i < lines.length) {
|
|
133
|
-
const line = lines[i];
|
|
134
|
-
if (!line) { i++; continue; }
|
|
135
|
-
if (line.startsWith("+") || line.startsWith("-") || line.startsWith(":")) {
|
|
136
|
-
responses.push(line);
|
|
137
|
-
i++;
|
|
138
|
-
} else if (line.startsWith("$")) {
|
|
139
|
-
const len = parseInt(line.slice(1), 10);
|
|
140
|
-
if (len === -1) { responses.push(null); i++; }
|
|
141
|
-
else { responses.push(lines[i+1] || ""); i += 2; }
|
|
142
|
-
} else { i++; }
|
|
143
|
-
}
|
|
144
|
-
// The last response is our actual command result
|
|
145
|
-
const result = responses[responses.length - 1];
|
|
146
|
-
if (result === null) process.stdout.write("__NULL__");
|
|
147
|
-
else if (typeof result === "string" && result.startsWith("-")) process.stdout.write("__ERR__" + result);
|
|
148
|
-
else process.stdout.write(String(result ?? "__NULL__"));
|
|
149
|
-
});
|
|
150
|
-
sock.on("error", (err) => {
|
|
151
|
-
process.stderr.write(err.message);
|
|
152
|
-
process.exit(1);
|
|
153
|
-
});
|
|
154
|
-
setTimeout(() => { sock.destroy(); process.exit(1); }, 3000);
|
|
155
|
-
`;
|
|
156
|
-
|
|
157
|
-
let result: string;
|
|
158
|
-
try {
|
|
159
|
-
result = execFileSync(process.execPath, ["-e", script], {
|
|
160
|
-
encoding: "utf-8",
|
|
161
|
-
timeout: 5000,
|
|
162
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
163
|
-
});
|
|
164
|
-
} catch (err) {
|
|
165
|
-
// Non-zero exit = the child hit a socket error / AUTH failure / timeout.
|
|
166
|
-
// That is a transport FAILURE, not a key miss — surface it so the
|
|
167
|
-
// Session boundary logs + degrades (or re-throws under strict mode).
|
|
168
|
-
throw new Error(`Valkey command failed: ${(err as Error).message}`);
|
|
169
|
-
}
|
|
170
|
-
if (result === "__NULL__") return ""; // genuine key miss
|
|
171
|
-
if (result.startsWith("__ERR__")) {
|
|
172
|
-
throw new Error(`Valkey error: ${result.slice("__ERR__".length)}`);
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
86
|
+
return respCommandSync(
|
|
87
|
+
{ host: this.host, port: this.port, password: this.password, db: this.db },
|
|
88
|
+
args,
|
|
89
|
+
"Valkey",
|
|
90
|
+
);
|
|
175
91
|
}
|
|
176
92
|
|
|
177
93
|
private key(sessionId: string): string {
|
|
@@ -278,12 +278,30 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
278
278
|
return rows[0] ?? null;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
insert(table: string, data: Record<string, unknown>): DatabaseResult {
|
|
281
|
+
insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult {
|
|
282
282
|
throw new Error("Use insertAsync() for Firebird.");
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
async insertAsync(table: string, data: Record<string, unknown>): Promise<DatabaseResult> {
|
|
285
|
+
async insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult> {
|
|
286
286
|
this.ensureConnected();
|
|
287
|
+
// A list of dicts is a batch insert — one parameterised INSERT run per row via
|
|
288
|
+
// executeManyAsync (ONE connection). Firebird has no generic last_insert_id, so
|
|
289
|
+
// the batch reports affectedRows == row count and no lastInsertId (same as the
|
|
290
|
+
// single-row path). See PostgresAdapter for the array-crash rationale.
|
|
291
|
+
if (Array.isArray(data)) {
|
|
292
|
+
if (data.length === 0) return { success: true, rowsAffected: 0 };
|
|
293
|
+
const keys = Object.keys(data[0]);
|
|
294
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
295
|
+
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
|
|
296
|
+
const paramsList = data.map((row) => keys.map((k) => row[k]));
|
|
297
|
+
try {
|
|
298
|
+
const result = await this.executeManyAsync(sql, paramsList);
|
|
299
|
+
return { success: true, rowsAffected: result.totalAffected, lastInsertId: result.lastInsertId };
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return { success: false, rowsAffected: 0, error: (e as Error).message };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
287
305
|
const keys = Object.keys(data);
|
|
288
306
|
const placeholders = keys.map(() => "?").join(", ");
|
|
289
307
|
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
|
|
@@ -236,12 +236,34 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
236
236
|
return rows[0] ?? null;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
insert(table: string, data: Record<string, unknown>): DatabaseResult {
|
|
239
|
+
insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult {
|
|
240
240
|
throw new Error("Use insertAsync() for MSSQL.");
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
async insertAsync(table: string, data: Record<string, unknown>): Promise<DatabaseResult> {
|
|
243
|
+
async insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult> {
|
|
244
244
|
this.ensureConnected();
|
|
245
|
+
// A list of dicts is a batch insert — one parameterised INSERT run per row via
|
|
246
|
+
// executeManyAsync (ONE connection). The single-row path appends SELECT
|
|
247
|
+
// SCOPE_IDENTITY() to surface the id; the batch path omits it (no per-row id is
|
|
248
|
+
// tracked for a batch — affectedRows == row count is what callers rely on).
|
|
249
|
+
// See PostgresAdapter for the array-crash rationale this branch fixes.
|
|
250
|
+
if (Array.isArray(data)) {
|
|
251
|
+
if (data.length === 0) return { success: true, rowsAffected: 0 };
|
|
252
|
+
const keys = Object.keys(data[0]);
|
|
253
|
+
// `?` placeholders — executeManyAsync -> executeAsync runs convertPlaceholders,
|
|
254
|
+
// which rewrites them to @p0, @p1, ... for tedious.
|
|
255
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
256
|
+
const sql = `INSERT INTO [${table}] ([${keys.join("], [")}]) VALUES (${placeholders})`;
|
|
257
|
+
const paramsList = data.map((row) => keys.map((k) => row[k]));
|
|
258
|
+
try {
|
|
259
|
+
const result = await this.executeManyAsync(sql, paramsList);
|
|
260
|
+
if (result.lastInsertId !== undefined) this._lastInsertId = result.lastInsertId;
|
|
261
|
+
return { success: true, rowsAffected: result.totalAffected, lastInsertId: result.lastInsertId };
|
|
262
|
+
} catch (e) {
|
|
263
|
+
return { success: false, rowsAffected: 0, error: (e as Error).message };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
245
267
|
const keys = Object.keys(data);
|
|
246
268
|
const placeholders = keys.map((_, i) => `@p${i}`).join(", ");
|
|
247
269
|
const sql = `INSERT INTO [${table}] ([${keys.join("], [")}]) VALUES (${placeholders}); SELECT SCOPE_IDENTITY() AS id`;
|
|
@@ -163,12 +163,30 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
163
163
|
return rows[0] ?? null;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
insert(table: string, data: Record<string, unknown>): DatabaseResult {
|
|
166
|
+
insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult {
|
|
167
167
|
throw new Error("Use insertAsync() for MySQL.");
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
async insertAsync(table: string, data: Record<string, unknown>): Promise<DatabaseResult> {
|
|
170
|
+
async insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult> {
|
|
171
171
|
this.ensureConnected();
|
|
172
|
+
// A list of dicts is a batch insert — one parameterised INSERT run per row via
|
|
173
|
+
// executeManyAsync (ONE connection). See PostgresAdapter for the rationale;
|
|
174
|
+
// without this branch a list crashed/mis-built SQL via Object.keys() on the array.
|
|
175
|
+
if (Array.isArray(data)) {
|
|
176
|
+
if (data.length === 0) return { success: true, rowsAffected: 0 };
|
|
177
|
+
const keys = Object.keys(data[0]);
|
|
178
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
179
|
+
const sql = `INSERT INTO \`${table}\` (\`${keys.join("`, `")}\`) VALUES (${placeholders})`;
|
|
180
|
+
const paramsList = data.map((row) => keys.map((k) => row[k]));
|
|
181
|
+
try {
|
|
182
|
+
const result = await this.executeManyAsync(sql, paramsList);
|
|
183
|
+
if (result.lastInsertId !== undefined) this._lastInsertId = result.lastInsertId;
|
|
184
|
+
return { success: true, rowsAffected: result.totalAffected, lastInsertId: result.lastInsertId };
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return { success: false, rowsAffected: 0, error: (e as Error).message };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
172
190
|
const keys = Object.keys(data);
|
|
173
191
|
const placeholders = keys.map(() => "?").join(", ");
|
|
174
192
|
const sql = `INSERT INTO \`${table}\` (\`${keys.join("`, `")}\`) VALUES (${placeholders})`;
|
|
@@ -68,7 +68,10 @@ export interface PostgresConfig {
|
|
|
68
68
|
|
|
69
69
|
export class PostgresAdapter implements DatabaseAdapter {
|
|
70
70
|
private client: InstanceType<typeof import("pg").Client> | null = null;
|
|
71
|
-
|
|
71
|
+
// string is included for non-integer primary keys: a UUID PK (the common
|
|
72
|
+
// `id uuid PRIMARY KEY DEFAULT gen_random_uuid()` shape) returns its id as a
|
|
73
|
+
// 36-char string via RETURNING, not a SERIAL integer (#256).
|
|
74
|
+
private _lastInsertId: number | bigint | string | null = null;
|
|
72
75
|
|
|
73
76
|
constructor(private config: PostgresConfig | string) {}
|
|
74
77
|
|
|
@@ -115,16 +118,20 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
115
118
|
|
|
116
119
|
/**
|
|
117
120
|
* Normalise an `id` column value (typed `unknown` because pg row values are
|
|
118
|
-
* `unknown`) into the
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
121
|
+
* `unknown`) into the shape `_lastInsertId` / `DatabaseResult.lastInsertId`
|
|
122
|
+
* expect. At runtime PG returns numeric PKs as number/bigint (the int8/numeric
|
|
123
|
+
* type parsers above coerce them to Number); a numeric string is coerced to a
|
|
124
|
+
* number so the SERIAL path always returns the integer id.
|
|
125
|
+
*
|
|
126
|
+
* A non-numeric string id — the UUID PK case (`id uuid PRIMARY KEY DEFAULT
|
|
127
|
+
* gen_random_uuid()`) returned via RETURNING — is preserved as-is so the
|
|
128
|
+
* insert surfaces the actual id instead of null (#256). null/undefined/empty
|
|
129
|
+
* still become null.
|
|
123
130
|
*/
|
|
124
|
-
private normalizeId(value: unknown): number | bigint | null {
|
|
131
|
+
private normalizeId(value: unknown): number | bigint | string | null {
|
|
125
132
|
if (typeof value === "number" || typeof value === "bigint") return value;
|
|
126
|
-
if (typeof value === "string" && value.trim() !== ""
|
|
127
|
-
return Number(value);
|
|
133
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
134
|
+
return Number.isNaN(Number(value)) ? value : Number(value);
|
|
128
135
|
}
|
|
129
136
|
return null;
|
|
130
137
|
}
|
|
@@ -202,12 +209,33 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
202
209
|
return rows[0] ?? null;
|
|
203
210
|
}
|
|
204
211
|
|
|
205
|
-
insert(table: string, data: Record<string, unknown>): DatabaseResult {
|
|
212
|
+
insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult {
|
|
206
213
|
throw new Error("Use insertAsync() for PostgreSQL.");
|
|
207
214
|
}
|
|
208
215
|
|
|
209
|
-
async insertAsync(table: string, data: Record<string, unknown>): Promise<DatabaseResult> {
|
|
216
|
+
async insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult> {
|
|
210
217
|
this.ensureConnected();
|
|
218
|
+
// A list of dicts is a batch insert — build one parameterised INSERT and run
|
|
219
|
+
// it once per row via executeManyAsync (ONE connection, wrapped in a single
|
|
220
|
+
// transaction). Database.insert / the docs advertise `data: object | object[]`;
|
|
221
|
+
// without this branch a list called Object.keys() on the array — `["0","1",…]`
|
|
222
|
+
// — producing garbage SQL (mirrors the Python `'list' has no attribute keys`
|
|
223
|
+
// crash this fix addresses).
|
|
224
|
+
if (Array.isArray(data)) {
|
|
225
|
+
if (data.length === 0) return { success: true, rowsAffected: 0 };
|
|
226
|
+
const keys = Object.keys(data[0]);
|
|
227
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
228
|
+
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
|
|
229
|
+
const paramsList = data.map((row) => keys.map((k) => row[k]));
|
|
230
|
+
try {
|
|
231
|
+
const result = await this.executeManyAsync(sql, paramsList);
|
|
232
|
+
if (result.lastInsertId !== undefined) this._lastInsertId = result.lastInsertId;
|
|
233
|
+
return { success: true, rowsAffected: result.totalAffected, lastInsertId: result.lastInsertId };
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return { success: false, rowsAffected: 0, error: (e as Error).message };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
211
239
|
const keys = Object.keys(data);
|
|
212
240
|
const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
|
|
213
241
|
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders}) RETURNING *`;
|
|
@@ -337,7 +365,7 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
337
365
|
}));
|
|
338
366
|
}
|
|
339
367
|
|
|
340
|
-
lastInsertId(): number | bigint | null {
|
|
368
|
+
lastInsertId(): number | bigint | string | null {
|
|
341
369
|
return this._lastInsertId;
|
|
342
370
|
}
|
|
343
371
|
|
|
@@ -18,9 +18,23 @@ function isIdentifier(str: string): boolean {
|
|
|
18
18
|
*/
|
|
19
19
|
type SqlParam = null | number | bigint | string | NodeJS.ArrayBufferView;
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Coerce adapter-level `unknown[]` params to node:sqlite's bindable shape.
|
|
23
|
+
*
|
|
24
|
+
* node:sqlite only binds null/number/bigint/string/ArrayBufferView and REJECTS a
|
|
25
|
+
* raw JS boolean ("Provided value cannot be bound to SQLite parameter N"). SQLite
|
|
26
|
+
* stores booleans as INTEGER 0/1 (fieldTypeToSQLite maps "boolean" -> INTEGER, and
|
|
27
|
+
* boolean defaults already emit 1/0), so coerce here at the single bind boundary —
|
|
28
|
+
* every write path (execute/query/fetchOne/insert/update/delete and ORM save())
|
|
29
|
+
* funnels through this. booleans -> 0/1, Date -> ISO-8601 string, undefined -> null.
|
|
30
|
+
*/
|
|
22
31
|
function toSqlParams(params: readonly unknown[]): SqlParam[] {
|
|
23
|
-
return params
|
|
32
|
+
return params.map((p) => {
|
|
33
|
+
if (typeof p === "boolean") return p ? 1 : 0;
|
|
34
|
+
if (p === undefined) return null;
|
|
35
|
+
if (p instanceof Date) return p.toISOString();
|
|
36
|
+
return p as SqlParam;
|
|
37
|
+
});
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
/**
|
|
@@ -64,6 +64,19 @@ export class AutoCrud {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Filter discovered models down to those that explicitly opted into auto-CRUD via
|
|
69
|
+
* `static autoCrud = true` (the documented opt-in gate; default false). The server
|
|
70
|
+
* passes only these to generateCrudRoutes, so a model without the flag gets no CRUD
|
|
71
|
+
* endpoints. Exported so the opt-in gate is locked in by a test rather than
|
|
72
|
+
* re-implemented at each call site.
|
|
73
|
+
*/
|
|
74
|
+
export function crudEligibleModels(models: DiscoveredModel[]): DiscoveredModel[] {
|
|
75
|
+
return models.filter(
|
|
76
|
+
(m) => (m.modelClass as { autoCrud?: boolean } | undefined)?.autoCrud === true,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
/**
|
|
68
81
|
* Generate CRUD route definitions for the given models.
|
|
69
82
|
* (Standalone function for backward compatibility.)
|
|
@@ -679,7 +679,9 @@ export class BaseModel {
|
|
|
679
679
|
// the caller; don't overwrite it with the driver's last_id.
|
|
680
680
|
if (pkField?.autoIncrement) {
|
|
681
681
|
// RETURNING result: pg puts it in rows[0][pkCol]; normalise here.
|
|
682
|
-
|
|
682
|
+
// string is allowed for a non-integer PK surfaced by lastInsertId()
|
|
683
|
+
// (e.g. a PostgreSQL UUID PK) — #256.
|
|
684
|
+
let newId: number | bigint | string | null = extractLastInsertId(result);
|
|
683
685
|
if (newId === null && result && typeof result === "object") {
|
|
684
686
|
const rows = (result as any).rows;
|
|
685
687
|
if (Array.isArray(rows) && rows[0]) {
|
|
@@ -397,7 +397,7 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
397
397
|
return this.adapter.columns(table);
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
lastInsertId(): number | bigint | null {
|
|
400
|
+
lastInsertId(): number | bigint | string | null {
|
|
401
401
|
return this.adapter.lastInsertId();
|
|
402
402
|
}
|
|
403
403
|
|
|
@@ -519,6 +519,19 @@ export class Database {
|
|
|
519
519
|
this.adapter = adapter;
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Set the engine type ("sqlite" | "postgres" | "mysql" | "mssql" |
|
|
524
|
+
* "firebird" | "mongodb"). The static `Database.create` factory assigns the
|
|
525
|
+
* private `dbType` directly; `initDatabase()` (a free function, no private
|
|
526
|
+
* access) routes through this setter so a URL connection is correctly typed.
|
|
527
|
+
* Without it a `postgres://` connection kept the `"sqlite"` default and
|
|
528
|
+
* `getNextId()` took the SQLite branch — hitting the non-existent
|
|
529
|
+
* `tina4_sequences` table on PostgreSQL instead of native sequences (#255).
|
|
530
|
+
*/
|
|
531
|
+
setDbType(type: string): void {
|
|
532
|
+
this.dbType = type;
|
|
533
|
+
}
|
|
534
|
+
|
|
522
535
|
/**
|
|
523
536
|
* Async factory: creates a Database from a connection URL.
|
|
524
537
|
* Works with all adapter types (sqlite, postgres, mysql, mssql, firebird).
|
|
@@ -755,8 +768,8 @@ export class Database {
|
|
|
755
768
|
}
|
|
756
769
|
}
|
|
757
770
|
|
|
758
|
-
/** Insert
|
|
759
|
-
async insert(table: string, data: Record<string, unknown>): Promise<DatabaseWriteResult> {
|
|
771
|
+
/** Insert one row (object) or a batch of rows (array of objects) into a table. */
|
|
772
|
+
async insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseWriteResult> {
|
|
760
773
|
const adapter = this.getNextAdapter();
|
|
761
774
|
const result = (adapter as any).insertAsync
|
|
762
775
|
? await (adapter as any).insertAsync(table, data)
|
|
@@ -1434,11 +1447,19 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1434
1447
|
if (pool > 0) {
|
|
1435
1448
|
// Pool-aware path — delegate to Database.create which manages
|
|
1436
1449
|
// round-robin adapter rotation and async-local-storage transaction pinning.
|
|
1437
|
-
// Database.create already exposes the global; exposeDb here
|
|
1450
|
+
// Database.create already sets dbType + exposes the global; exposeDb here
|
|
1451
|
+
// is idempotent.
|
|
1438
1452
|
return exposeDb(await Database.create(url, resolvedUser, resolvedPassword, pool));
|
|
1439
1453
|
}
|
|
1454
|
+
// Single-connection URL path. Parse the URL so the engine type is known and
|
|
1455
|
+
// set it on the Database — otherwise dbType keeps its "sqlite" default and a
|
|
1456
|
+
// postgres://… connection takes the SQLite getNextId branch, crashing on the
|
|
1457
|
+
// missing tina4_sequences table instead of using native sequences (#255).
|
|
1458
|
+
const parsed = parseDatabaseUrl(url, resolvedUser, resolvedPassword);
|
|
1440
1459
|
const adapter = await createAdapterFromUrl(url, resolvedUser, resolvedPassword);
|
|
1441
|
-
|
|
1460
|
+
const db = new Database(setAdapter(adapter));
|
|
1461
|
+
db.setDbType(parsed.type);
|
|
1462
|
+
return exposeDb(db);
|
|
1442
1463
|
}
|
|
1443
1464
|
|
|
1444
1465
|
// Legacy config path — normalize "sqlserver" to "mssql"
|
|
@@ -1459,11 +1480,21 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1459
1480
|
);
|
|
1460
1481
|
}
|
|
1461
1482
|
|
|
1483
|
+
// Legacy config-object path. As with the URL path above, the constructed
|
|
1484
|
+
// Database must be told its engine type — otherwise dbType keeps its "sqlite"
|
|
1485
|
+
// default and a `{ type: "postgres" }` connection takes the SQLite getNextId
|
|
1486
|
+
// branch and crashes on the missing tina4_sequences table (#255).
|
|
1487
|
+
const finished = (adapter: DatabaseAdapter): Database => {
|
|
1488
|
+
const db = new Database(setAdapter(adapter));
|
|
1489
|
+
db.setDbType(type);
|
|
1490
|
+
return exposeDb(db);
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1462
1493
|
switch (type) {
|
|
1463
1494
|
case "sqlite": {
|
|
1464
1495
|
const { SQLiteAdapter } = await import("./adapters/sqlite.js");
|
|
1465
1496
|
const adapter = new SQLiteAdapter(config?.path ?? "./data/tina4.db");
|
|
1466
|
-
return
|
|
1497
|
+
return finished(adapter);
|
|
1467
1498
|
}
|
|
1468
1499
|
case "postgres": {
|
|
1469
1500
|
const { PostgresAdapter } = await import("./adapters/postgres.js");
|
|
@@ -1475,7 +1506,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1475
1506
|
database: config?.database,
|
|
1476
1507
|
});
|
|
1477
1508
|
await adapter.connect();
|
|
1478
|
-
return
|
|
1509
|
+
return finished(adapter);
|
|
1479
1510
|
}
|
|
1480
1511
|
case "mysql": {
|
|
1481
1512
|
const { MysqlAdapter } = await import("./adapters/mysql.js");
|
|
@@ -1487,7 +1518,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1487
1518
|
database: config?.database,
|
|
1488
1519
|
});
|
|
1489
1520
|
await adapter.connect();
|
|
1490
|
-
return
|
|
1521
|
+
return finished(adapter);
|
|
1491
1522
|
}
|
|
1492
1523
|
case "mssql": {
|
|
1493
1524
|
const { MssqlAdapter } = await import("./adapters/mssql.js");
|
|
@@ -1499,7 +1530,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1499
1530
|
database: config?.database,
|
|
1500
1531
|
});
|
|
1501
1532
|
await adapter.connect();
|
|
1502
|
-
return
|
|
1533
|
+
return finished(adapter);
|
|
1503
1534
|
}
|
|
1504
1535
|
case "firebird": {
|
|
1505
1536
|
const { FirebirdAdapter } = await import("./adapters/firebird.js");
|
|
@@ -1511,7 +1542,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1511
1542
|
database: config?.database,
|
|
1512
1543
|
});
|
|
1513
1544
|
await adapter.connect();
|
|
1514
|
-
return
|
|
1545
|
+
return finished(adapter);
|
|
1515
1546
|
}
|
|
1516
1547
|
case "mongodb": {
|
|
1517
1548
|
const { MongodbAdapter } = await import("./adapters/mongodb.js");
|
|
@@ -1524,14 +1555,14 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1524
1555
|
const connectionString = `mongodb://${creds}${host}:${port}/${database}`;
|
|
1525
1556
|
const adapter = new MongodbAdapter(connectionString);
|
|
1526
1557
|
await adapter.connect();
|
|
1527
|
-
return
|
|
1558
|
+
return finished(adapter);
|
|
1528
1559
|
}
|
|
1529
1560
|
case "odbc": {
|
|
1530
1561
|
const { OdbcAdapter } = await import("./adapters/odbc.js");
|
|
1531
1562
|
const connStr = config?.connectionString ?? config?.url?.replace(/^odbc:\/\/\//, "") ?? "";
|
|
1532
1563
|
const adapter = new OdbcAdapter({ connectionString: connStr });
|
|
1533
1564
|
await adapter.connect();
|
|
1534
|
-
return
|
|
1565
|
+
return finished(adapter);
|
|
1535
1566
|
}
|
|
1536
1567
|
default:
|
|
1537
1568
|
throw new Error(`Unknown database type: ${type}`);
|
|
@@ -45,7 +45,7 @@ export {
|
|
|
45
45
|
shouldSkipCreateTable,
|
|
46
46
|
} from "./migration.js";
|
|
47
47
|
export type { MigrationResult, MigrationStatus } from "./migration.js";
|
|
48
|
-
export { AutoCrud, generateCrudRoutes } from "./autoCrud.js";
|
|
48
|
+
export { AutoCrud, generateCrudRoutes, crudEligibleModels } from "./autoCrud.js";
|
|
49
49
|
export { buildQuery, parseQueryString } from "./query.js";
|
|
50
50
|
export { validate } from "./validation.js";
|
|
51
51
|
export type { ValidationError } from "./validation.js";
|