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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One CRUD SQL builder for every engine, instead of one per adapter.
|
|
3
|
+
*
|
|
4
|
+
* Feature 3's last open item, the 4.3x LOC finding: `insert`/`update`/`delete`
|
|
5
|
+
* built their SQL independently in all seven adapters. Building
|
|
6
|
+
* `INSERT INTO x (a, b) VALUES (?, ?)` is not engine-specific work - Ruby has
|
|
7
|
+
* always done it once - and the seven copies differed in exactly two ways:
|
|
8
|
+
*
|
|
9
|
+
* IDENTIFIER QUOTING "col" | `col` | [col] | Firebird's fbQuote
|
|
10
|
+
* PARAMETER MARKER ? | $1 | @p1
|
|
11
|
+
*
|
|
12
|
+
* Both are captured in a `Dialect` below, so the builders are shared and each
|
|
13
|
+
* adapter declares only what genuinely differs about its engine.
|
|
14
|
+
*
|
|
15
|
+
* These functions build STRINGS and nothing else. Execution and result
|
|
16
|
+
* extraction stay in the adapters on purpose: those really are per-driver
|
|
17
|
+
* (`client.query` vs `lastInsertRowid` vs a Firebird transaction handle), and
|
|
18
|
+
* folding them in here would trade a real duplication for a fake abstraction.
|
|
19
|
+
*
|
|
20
|
+
* MongoDB has no entry: it does not build SQL at all.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** How one engine spells identifiers and parameter markers. */
|
|
24
|
+
export interface Dialect {
|
|
25
|
+
/** Quote a table or column name for this engine. */
|
|
26
|
+
quote(name: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* The parameter marker for the 1-based position `index`. Engines with
|
|
29
|
+
* positional markers ($1, @p1) use the index; the rest ignore it.
|
|
30
|
+
*/
|
|
31
|
+
marker(index: number): string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const doubleQuote = (name: string): string => `"${name}"`;
|
|
35
|
+
const questionMark = (): string => "?";
|
|
36
|
+
|
|
37
|
+
/** SQLite, and ODBC which follows the SQL standard spelling. */
|
|
38
|
+
export const ANSI_DIALECT: Dialect = { quote: doubleQuote, marker: questionMark };
|
|
39
|
+
|
|
40
|
+
/** PostgreSQL: standard quoting, positional $N markers. */
|
|
41
|
+
export const POSTGRES_DIALECT: Dialect = {
|
|
42
|
+
quote: doubleQuote,
|
|
43
|
+
marker: (index) => `$${index}`,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** MySQL: backtick quoting. */
|
|
47
|
+
export const MYSQL_DIALECT: Dialect = {
|
|
48
|
+
quote: (name) => `\`${name}\``,
|
|
49
|
+
marker: questionMark,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** MSSQL: bracket quoting, named @pN markers. */
|
|
53
|
+
export const MSSQL_DIALECT: Dialect = {
|
|
54
|
+
quote: (name) => `[${name}]`,
|
|
55
|
+
marker: (index) => `@p${index}`,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Firebird quotes only when it has to: an unquoted identifier is folded to
|
|
60
|
+
* UPPER CASE, so quoting a lower-case name would make it unfindable. The
|
|
61
|
+
* adapter owns that rule and passes its own quoter in.
|
|
62
|
+
*/
|
|
63
|
+
export function firebirdDialect(fbQuote: (name: string) => string): Dialect {
|
|
64
|
+
return { quote: fbQuote, marker: questionMark };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `INSERT INTO <table> (<cols>) VALUES (<markers>)`.
|
|
69
|
+
*
|
|
70
|
+
* @param suffix Appended verbatim - PostgreSQL passes " RETURNING *" and MSSQL
|
|
71
|
+
* its SCOPE_IDENTITY() probe, the genuinely engine-specific parts.
|
|
72
|
+
* @param startAt Position of the FIRST marker. PostgreSQL numbers its `$N` from
|
|
73
|
+
* 1; MSSQL names its `@pN` from 0 and BINDS by that same name, so
|
|
74
|
+
* shifting it would produce SQL whose parameters do not exist.
|
|
75
|
+
* Engines using `?` ignore this.
|
|
76
|
+
*/
|
|
77
|
+
export function buildInsert(
|
|
78
|
+
dialect: Dialect,
|
|
79
|
+
table: string,
|
|
80
|
+
keys: string[],
|
|
81
|
+
suffix = "",
|
|
82
|
+
startAt = 1,
|
|
83
|
+
): string {
|
|
84
|
+
const columns = keys.map((k) => dialect.quote(k)).join(", ");
|
|
85
|
+
const placeholders = keys.map((_, i) => dialect.marker(startAt + i)).join(", ");
|
|
86
|
+
return `INSERT INTO ${dialect.quote(table)} (${columns}) VALUES (${placeholders})${suffix}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The `SET a = ?, b = ?` fragment of an UPDATE.
|
|
91
|
+
*
|
|
92
|
+
* @param startAt 1-based position of the FIRST marker. An UPDATE's WHERE
|
|
93
|
+
* clause continues the numbering after the SET values, so a
|
|
94
|
+
* positional engine ($N, @pN) must not restart at 1.
|
|
95
|
+
*/
|
|
96
|
+
export function buildSetClause(
|
|
97
|
+
dialect: Dialect,
|
|
98
|
+
keys: string[],
|
|
99
|
+
startAt = 1,
|
|
100
|
+
): string {
|
|
101
|
+
return keys
|
|
102
|
+
.map((k, i) => `${dialect.quote(k)} = ${dialect.marker(startAt + i)}`)
|
|
103
|
+
.join(", ");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The `a = ? AND b = ?` fragment for an object filter.
|
|
108
|
+
*
|
|
109
|
+
* @param startAt 1-based position of the first marker, for the same reason as
|
|
110
|
+
* buildSetClause.
|
|
111
|
+
*/
|
|
112
|
+
export function buildWhereClause(
|
|
113
|
+
dialect: Dialect,
|
|
114
|
+
keys: string[],
|
|
115
|
+
startAt = 1,
|
|
116
|
+
): string {
|
|
117
|
+
return keys
|
|
118
|
+
.map((k, i) => `${dialect.quote(k)} = ${dialect.marker(startAt + i)}`)
|
|
119
|
+
.join(" AND ");
|
|
120
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { ANSI_DIALECT, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
|
|
2
3
|
import { mkdirSync } from "node:fs";
|
|
3
4
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
5
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
@@ -99,6 +100,18 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
99
100
|
private db: DatabaseSync;
|
|
100
101
|
private _lastInsertId: number | bigint | null = null;
|
|
101
102
|
|
|
103
|
+
/**
|
|
104
|
+
* TINA4_DATABASE_CONNECT_TIMEOUT DOES NOT APPLY HERE, deliberately.
|
|
105
|
+
*
|
|
106
|
+
* There is no connect() to bound: `node:sqlite` opens the file in this
|
|
107
|
+
* SYNCHRONOUS constructor, and a synchronous call cannot be interrupted by a
|
|
108
|
+
* timer on the same thread - the event loop only gets to run the timer after
|
|
109
|
+
* `new DatabaseSync()` has already returned. There is also no host and no port
|
|
110
|
+
* to name in a timeout error. The one case that could still block is a local
|
|
111
|
+
* file on a wedged network mount, which is a kernel-level stall no JS bound
|
|
112
|
+
* can reach. Stated here so the exclusion reads as a decision rather than an
|
|
113
|
+
* adapter somebody forgot.
|
|
114
|
+
*/
|
|
102
115
|
constructor(dbPath: string) {
|
|
103
116
|
const resolved = resolveSqlitePath(dbPath);
|
|
104
117
|
this.db = new DatabaseSync(resolved);
|
|
@@ -145,17 +158,13 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
effectiveSql += ` OFFSET ${skip}`;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
161
|
+
// SQLTranslator.appendLimit owns BOTH halves of this decision: it scrubs
|
|
162
|
+
// literals and comments before asking "does the caller already have a
|
|
163
|
+
// LIMIT?", and it appends on a new line so the clause can never land inside
|
|
164
|
+
// a trailing `--` comment. The old inline version did neither, so
|
|
165
|
+
// `WHERE label != 'LIMIT'` and a trailing `-- LIMIT 5` each returned the
|
|
166
|
+
// WHOLE TABLE instead of the 100-row cap.
|
|
167
|
+
const effectiveSql = SQLTranslator.appendLimit(sql, limit, skip);
|
|
159
168
|
return this.query<T>(effectiveSql, params);
|
|
160
169
|
}
|
|
161
170
|
|
|
@@ -169,16 +178,14 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
169
178
|
if (Array.isArray(data)) {
|
|
170
179
|
if (data.length === 0) return { success: true, affectedRows: 0 };
|
|
171
180
|
const keys = Object.keys(data[0]);
|
|
172
|
-
const
|
|
173
|
-
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
|
|
181
|
+
const sql = buildInsert(ANSI_DIALECT, table, keys);
|
|
174
182
|
const paramsList = data.map((row) => keys.map((k) => row[k]));
|
|
175
183
|
const result = this.executeMany(sql, paramsList);
|
|
176
184
|
return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
const keys = Object.keys(data);
|
|
180
|
-
const
|
|
181
|
-
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
|
|
188
|
+
const sql = buildInsert(ANSI_DIALECT, table, keys);
|
|
182
189
|
const values = Object.values(data);
|
|
183
190
|
|
|
184
191
|
try {
|
|
@@ -190,10 +197,30 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
199
|
|
|
193
|
-
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown
|
|
194
|
-
const setClauses = Object.keys(data)
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): DatabaseResult {
|
|
201
|
+
const setClauses = buildSetClause(ANSI_DIALECT, Object.keys(data));
|
|
202
|
+
|
|
203
|
+
// A raw WHERE fragment + params is half the write_path contract's filter
|
|
204
|
+
// form ("a string filter with params works the same as a hash filter").
|
|
205
|
+
// Without this branch Object.keys("id = ?") yields the STRING INDICES
|
|
206
|
+
// ["0","1",...], producing `WHERE "0" = ? AND "1" = ?` and SQLite reports
|
|
207
|
+
// `no such column: "0"`. delete() below already carried this branch and
|
|
208
|
+
// update() did not — the same gap 3.13.94 closed in the postgres/mysql/
|
|
209
|
+
// mssql/firebird adapters, still open here on the DEFAULT engine.
|
|
210
|
+
if (typeof filter === "string") {
|
|
211
|
+
const where = filter ? ` WHERE ${filter}` : "";
|
|
212
|
+
const sql = `UPDATE ${ANSI_DIALECT.quote(table)} SET ${setClauses}${where}`;
|
|
213
|
+
const values = [...Object.values(data), ...(params ?? [])];
|
|
214
|
+
try {
|
|
215
|
+
const result = this.db.prepare(sql).run(...toSqlParams(values));
|
|
216
|
+
return { success: true, affectedRows: Number(result.changes) };
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
|
|
223
|
+
const sql = `UPDATE ${ANSI_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereClauses}`;
|
|
197
224
|
const values = [...Object.values(data), ...Object.values(filter)];
|
|
198
225
|
|
|
199
226
|
try {
|
|
@@ -224,8 +251,8 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
224
251
|
}
|
|
225
252
|
}
|
|
226
253
|
|
|
227
|
-
const whereClauses = Object.keys(filter)
|
|
228
|
-
const sql = `DELETE FROM
|
|
254
|
+
const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
|
|
255
|
+
const sql = `DELETE FROM ${ANSI_DIALECT.quote(table)} WHERE ${whereClauses}`;
|
|
229
256
|
const values = Object.values(filter);
|
|
230
257
|
|
|
231
258
|
try {
|
|
@@ -260,14 +287,14 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
260
287
|
this._inTransaction = false;
|
|
261
288
|
}
|
|
262
289
|
|
|
263
|
-
|
|
290
|
+
getTables(): string[] {
|
|
264
291
|
const rows = this.query<{ name: string }>(
|
|
265
292
|
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
|
|
266
293
|
);
|
|
267
294
|
return rows.map((r) => r.name);
|
|
268
295
|
}
|
|
269
296
|
|
|
270
|
-
|
|
297
|
+
getColumns(table: string): ColumnInfo[] {
|
|
271
298
|
// v3.13.14 (#48): a SQLite "schema" is an ATTACH alias ("extra.widget").
|
|
272
299
|
// PRAGMA accepts a schema prefix when both parts are plain identifiers.
|
|
273
300
|
const [schema, tbl] = SQLTranslator.splitSchema(table);
|
|
@@ -279,7 +306,10 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
279
306
|
name: string; type: string; notnull: number; dflt_value: unknown; pk: number;
|
|
280
307
|
}>;
|
|
281
308
|
return rows.map((r) => ({
|
|
282
|
-
|
|
309
|
+
// PRAGMA table_info reports `pk` as the 1-BASED POSITION within the primary
|
|
310
|
+
// key, not a boolean: a composite key gives pk=1, pk=2, ... Testing `=== 1`
|
|
311
|
+
// reported only the first column of a composite key.
|
|
312
|
+
name: r.name, type: r.type, nullable: r.notnull === 0, default: r.dflt_value, primaryKey: Number(r.pk) > 0,
|
|
283
313
|
}));
|
|
284
314
|
}
|
|
285
315
|
|
|
@@ -360,10 +390,16 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
360
390
|
|
|
361
391
|
createTable(name: string, columns: Record<string, FieldDefinition>): void {
|
|
362
392
|
const colDefs: string[] = [];
|
|
393
|
+
// A COMPOSITE key is declared ONCE, at table level (below). An inline
|
|
394
|
+
// PRIMARY KEY per column is invalid DDL - SQLite rejects it outright with
|
|
395
|
+
// "table X has more than one primary key", so a composite-key model could
|
|
396
|
+
// not create its own table at all.
|
|
397
|
+
const pkCols = Object.entries(columns).filter(([, d]) => d.primaryKey).map(([c]) => c);
|
|
398
|
+
const composite = pkCols.length > 1;
|
|
363
399
|
for (const [colName, def] of Object.entries(columns)) {
|
|
364
400
|
const sqlType = fieldTypeToSQLite(def.type);
|
|
365
401
|
const parts = [`"${colName}" ${sqlType}`];
|
|
366
|
-
if (def.primaryKey) parts.push("PRIMARY KEY");
|
|
402
|
+
if (def.primaryKey && !composite) parts.push("PRIMARY KEY");
|
|
367
403
|
if (def.autoIncrement) parts.push("AUTOINCREMENT");
|
|
368
404
|
if (def.required && !def.primaryKey) parts.push("NOT NULL");
|
|
369
405
|
// A json column carries no DDL DEFAULT (parity with the Python master): an
|
|
@@ -372,6 +408,9 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
372
408
|
if (def.type !== "json" && def.default === "now") parts.push("DEFAULT CURRENT_TIMESTAMP");
|
|
373
409
|
colDefs.push(parts.join(" "));
|
|
374
410
|
}
|
|
411
|
+
if (composite) {
|
|
412
|
+
colDefs.push(`PRIMARY KEY (${pkCols.map((c) => `"${c}"`).join(", ")})`);
|
|
413
|
+
}
|
|
375
414
|
this.db.exec(`CREATE TABLE IF NOT EXISTS "${name}" (${colDefs.join(", ")})`);
|
|
376
415
|
}
|
|
377
416
|
|
|
@@ -3,11 +3,12 @@ import {
|
|
|
3
3
|
adapterQuery, adapterFetch, adapterExecute, adapterFetchOne,
|
|
4
4
|
adapterStartTransaction, adapterCommit, adapterRollback,
|
|
5
5
|
adapterTableExists, adapterCreateTable, extractLastInsertId,
|
|
6
|
+
DEFAULT_ROW_CAP,
|
|
6
7
|
} from "./database.js";
|
|
7
8
|
import { validate as validateFields } from "./validation.js";
|
|
8
9
|
import { QueryBuilder } from "./queryBuilder.js";
|
|
9
10
|
import { SQLiteAdapter } from "./adapters/sqlite.js";
|
|
10
|
-
import { QueryCache } from "./sqlTranslator.js";
|
|
11
|
+
import { QueryCache, SQLTranslator } from "./sqlTranslator.js";
|
|
11
12
|
import { Log } from "../../core/src/index.js";
|
|
12
13
|
import type { DatabaseAdapter, FieldDefinition, RelationshipDefinition } from "./types.js";
|
|
13
14
|
|
|
@@ -317,15 +318,17 @@ export class BaseModel {
|
|
|
317
318
|
const url = process.env.TINA4_DATABASE_URL;
|
|
318
319
|
if (url) {
|
|
319
320
|
const parsed = parseDatabaseUrl(url);
|
|
320
|
-
if (parsed.
|
|
321
|
+
if (parsed.engine === "sqlite") {
|
|
321
322
|
// SQLite adapter is synchronous — create it inline and register as default
|
|
322
|
-
const dbPath = parsed.
|
|
323
|
-
|
|
323
|
+
const dbPath = parsed.database || "./data/tina4.db";
|
|
324
|
+
// Typed as the INTERFACE so the optional identity tag is assignable.
|
|
325
|
+
const adapter: DatabaseAdapter = new SQLiteAdapter(dbPath);
|
|
326
|
+
adapter.cacheIdentity = QueryCache.cacheIdentity(url);
|
|
324
327
|
setAdapter(adapter);
|
|
325
328
|
return adapter;
|
|
326
329
|
}
|
|
327
330
|
throw new Error(
|
|
328
|
-
`TINA4_DATABASE_URL is set to a non-SQLite engine ("${parsed.
|
|
331
|
+
`TINA4_DATABASE_URL is set to a non-SQLite engine ("${parsed.engine}"). ` +
|
|
329
332
|
`Call await initDatabase() at startup before using ORM models.`,
|
|
330
333
|
);
|
|
331
334
|
}
|
|
@@ -342,6 +345,34 @@ export class BaseModel {
|
|
|
342
345
|
return Object.entries(this.fields).find(([, def]) => def.primaryKey)?.[0] ?? "id";
|
|
343
346
|
}
|
|
344
347
|
|
|
348
|
+
/**
|
|
349
|
+
* EVERY primary-key field name, in declaration order.
|
|
350
|
+
*
|
|
351
|
+
* A key may span several columns. `getPkField()` returns only the FIRST and
|
|
352
|
+
* is kept for the auto-increment paths, which are single-column by
|
|
353
|
+
* definition. Anything that ADDRESSES a row must use this: keying on one
|
|
354
|
+
* column of a composite key matches every row sharing that value, which is
|
|
355
|
+
* the data-loss shape feature 4 removed from the raw write path below.
|
|
356
|
+
*/
|
|
357
|
+
protected static getPkFields(): string[] {
|
|
358
|
+
const keys = Object.entries(this.fields)
|
|
359
|
+
.filter(([, def]) => def.primaryKey)
|
|
360
|
+
.map(([name]) => name);
|
|
361
|
+
return keys.length > 0 ? keys : ["id"];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** A WHERE naming EVERY primary-key column, and its bound params. */
|
|
365
|
+
protected pkWhere(): { sql: string; params: unknown[] } {
|
|
366
|
+
const ModelClass = this.constructor as typeof BaseModel;
|
|
367
|
+
const clauses: string[] = [];
|
|
368
|
+
const params: unknown[] = [];
|
|
369
|
+
for (const name of ModelClass.getPkFields()) {
|
|
370
|
+
clauses.push(`${ModelClass.getDbColumn(name)} = ?`);
|
|
371
|
+
params.push((this as Record<string, unknown>)[name]);
|
|
372
|
+
}
|
|
373
|
+
return { sql: clauses.join(" AND "), params };
|
|
374
|
+
}
|
|
375
|
+
|
|
345
376
|
/**
|
|
346
377
|
* Get the primary key database column name (applies fieldMapping).
|
|
347
378
|
*/
|
|
@@ -538,16 +569,37 @@ export class BaseModel {
|
|
|
538
569
|
}
|
|
539
570
|
|
|
540
571
|
/**
|
|
541
|
-
* Find all records
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
572
|
+
* Find all records.
|
|
573
|
+
*
|
|
574
|
+
* BREAKING (3.13.95, parity): the signature is now
|
|
575
|
+
* `all(limit?, offset?, include?, orderBy?)`. It NO LONGER accepts leading
|
|
576
|
+
* `where`/`params`.
|
|
577
|
+
*
|
|
578
|
+
* Node was the sole outlier of the four. The master and the other two never
|
|
579
|
+
* had a filter on `all()`:
|
|
580
|
+
* Python all(limit=100, offset=0, include=None, order_by=None)
|
|
581
|
+
* PHP all(int $limit = 100, int $offset = 0, ?array $include, ?string $orderBy)
|
|
582
|
+
* Ruby all(limit: 100, offset: nil, order_by: nil, include: nil)
|
|
583
|
+
* Node's extra leading parameters shifted every argument, so the same
|
|
584
|
+
* positional call meant different things in different languages -- which is
|
|
585
|
+
* precisely what the parity mandate exists to prevent.
|
|
586
|
+
*
|
|
587
|
+
* MIGRATION: a filtered read moves to `where()`, which already exists and
|
|
588
|
+
* takes the conditions first:
|
|
589
|
+
* before: User.all("age > ?", [28])
|
|
590
|
+
* after: User.where("age > ?", [28])
|
|
591
|
+
* TypeScript callers get a compile error (string is not assignable to number),
|
|
592
|
+
* so the break is loud rather than silent.
|
|
593
|
+
*
|
|
594
|
+
* @param limit Max records (default 100, the shared cross-framework cap).
|
|
595
|
+
* @param offset Records to skip (default 0).
|
|
596
|
+
* @param include Relationship names to eager-load.
|
|
597
|
+
* @param orderBy ORDER BY clause (e.g. "name ASC").
|
|
546
598
|
*/
|
|
547
599
|
static async all<T extends BaseModel>(
|
|
548
600
|
this: new (data?: Record<string, unknown>) => T,
|
|
549
|
-
|
|
550
|
-
|
|
601
|
+
limit: number = DEFAULT_ROW_CAP,
|
|
602
|
+
offset: number = 0,
|
|
551
603
|
include?: string[],
|
|
552
604
|
orderBy?: string,
|
|
553
605
|
): Promise<T[]> {
|
|
@@ -561,15 +613,16 @@ export class BaseModel {
|
|
|
561
613
|
if (ModelClass.tableFilter) {
|
|
562
614
|
conditions.push(ModelClass.tableFilter);
|
|
563
615
|
}
|
|
564
|
-
if (where) {
|
|
565
|
-
conditions.push(where);
|
|
566
|
-
}
|
|
567
616
|
|
|
568
617
|
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
569
618
|
const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
|
|
570
|
-
const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause}
|
|
619
|
+
const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause}`
|
|
620
|
+
+ ` LIMIT ${limit} OFFSET ${offset}`;
|
|
571
621
|
|
|
572
|
-
|
|
622
|
+
// No bind parameters: the only conditions left are the framework's own
|
|
623
|
+
// softDelete / tableFilter literals. A caller-supplied filter belongs on
|
|
624
|
+
// where(), which binds its params properly.
|
|
625
|
+
const rows = await adapterQuery(db, sql, []);
|
|
573
626
|
const instances = rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
574
627
|
if (include) {
|
|
575
628
|
await ModelClass._eagerLoad(instances, include);
|
|
@@ -583,7 +636,7 @@ export class BaseModel {
|
|
|
583
636
|
*
|
|
584
637
|
* @param conditions WHERE clause (e.g. "age > ? AND active = ?")
|
|
585
638
|
* @param params Bind parameters
|
|
586
|
-
* @param limit Max records (default
|
|
639
|
+
* @param limit Max records (default 100)
|
|
587
640
|
* @param offset Skip records (default 0)
|
|
588
641
|
* @param include Relationship names to eager-load
|
|
589
642
|
* @param orderBy ORDER BY clause (e.g. "name ASC")
|
|
@@ -592,7 +645,7 @@ export class BaseModel {
|
|
|
592
645
|
this: new (data?: Record<string, unknown>) => T,
|
|
593
646
|
conditions: string,
|
|
594
647
|
params?: unknown[],
|
|
595
|
-
limit: number =
|
|
648
|
+
limit: number = DEFAULT_ROW_CAP,
|
|
596
649
|
offset: number = 0,
|
|
597
650
|
include?: string[],
|
|
598
651
|
orderBy?: string,
|
|
@@ -676,7 +729,23 @@ export class BaseModel {
|
|
|
676
729
|
isUpdate = true;
|
|
677
730
|
} else {
|
|
678
731
|
try {
|
|
679
|
-
|
|
732
|
+
// This asked exists(pkValue), which tests only the FIRST key column.
|
|
733
|
+
// On a composite key that is true for any row sharing that column, so
|
|
734
|
+
// inserting a genuinely NEW row was decided to be an UPDATE and
|
|
735
|
+
// silently OVERWROTE a different row: saving (acme, a2) rewrote
|
|
736
|
+
// (acme, a1). The check has to name the whole key, like the write
|
|
737
|
+
// that follows it.
|
|
738
|
+
const probe = this.pkWhere();
|
|
739
|
+
if (ModelClass.getPkFields().length > 1 && probe.sql) {
|
|
740
|
+
const found = await db.fetch(
|
|
741
|
+
`SELECT 1 AS present FROM ${ModelClass.tableName} WHERE ${probe.sql}`,
|
|
742
|
+
probe.params,
|
|
743
|
+
1,
|
|
744
|
+
);
|
|
745
|
+
isUpdate = (found as unknown as { length: number }).length > 0;
|
|
746
|
+
} else {
|
|
747
|
+
isUpdate = await ModelClass.exists(pkValue);
|
|
748
|
+
}
|
|
680
749
|
} catch {
|
|
681
750
|
// If we can't tell (e.g. table doesn't exist yet), fall back
|
|
682
751
|
// to INSERT so the user sees the real driver error rather
|
|
@@ -689,16 +758,22 @@ export class BaseModel {
|
|
|
689
758
|
await adapterStartTransaction(db);
|
|
690
759
|
try {
|
|
691
760
|
if (isUpdate) {
|
|
692
|
-
// Update
|
|
761
|
+
// Update — keyed on the WHOLE primary key (see pkWhere).
|
|
693
762
|
const updateFields = Object.entries(ModelClass.fields).filter(
|
|
694
763
|
([name, def]) => !def.primaryKey && this[name] !== undefined,
|
|
695
764
|
);
|
|
696
765
|
if (updateFields.length === 0) { await adapterCommit(db); return this; }
|
|
697
766
|
|
|
698
767
|
const setClause = updateFields.map(([k]) => `"${ModelClass.getDbColumn(k)}" = ?`).join(", ");
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
768
|
+
// The key params come from pkWhere() below; appending pkValue here too
|
|
769
|
+
// would bind the first key column twice and shift every placeholder.
|
|
770
|
+
const values = [...updateFields.map(([k, def]) => toDbFieldValue(def, this[k]))];
|
|
771
|
+
|
|
772
|
+
// Keyed on the WHOLE primary key: one column of a composite key matches
|
|
773
|
+
// every row sharing that value.
|
|
774
|
+
const uw = this.pkWhere();
|
|
775
|
+
values.push(...uw.params);
|
|
776
|
+
await adapterExecute(db, `UPDATE "${ModelClass.tableName}" SET ${setClause} WHERE ${uw.sql}`, values);
|
|
702
777
|
} else {
|
|
703
778
|
// Insert
|
|
704
779
|
//
|
|
@@ -854,14 +929,14 @@ export class BaseModel {
|
|
|
854
929
|
try {
|
|
855
930
|
if (ModelClass.softDelete) {
|
|
856
931
|
await adapterExecute(db,
|
|
857
|
-
`UPDATE "${ModelClass.tableName}" SET is_deleted = 1 WHERE
|
|
858
|
-
|
|
932
|
+
`UPDATE "${ModelClass.tableName}" SET is_deleted = 1 WHERE ${this.pkWhere().sql}`,
|
|
933
|
+
this.pkWhere().params,
|
|
859
934
|
);
|
|
860
935
|
this.is_deleted = 1;
|
|
861
936
|
} else {
|
|
862
937
|
await adapterExecute(db,
|
|
863
|
-
`DELETE FROM "${ModelClass.tableName}" WHERE
|
|
864
|
-
|
|
938
|
+
`DELETE FROM "${ModelClass.tableName}" WHERE ${this.pkWhere().sql}`,
|
|
939
|
+
this.pkWhere().params,
|
|
865
940
|
);
|
|
866
941
|
}
|
|
867
942
|
await adapterCommit(db);
|
|
@@ -1047,7 +1122,10 @@ export class BaseModel {
|
|
|
1047
1122
|
const dbCol = this.getDbColumn(fieldName);
|
|
1048
1123
|
const sqlType = typeMap[def.type] || "TEXT";
|
|
1049
1124
|
const parts = [`"${dbCol}" ${sqlType}`];
|
|
1050
|
-
|
|
1125
|
+
// A COMPOSITE key is declared ONCE, at table level (below). An inline
|
|
1126
|
+
// PRIMARY KEY per column is invalid DDL - SQLite, PostgreSQL and MySQL
|
|
1127
|
+
// all reject two of them in one table.
|
|
1128
|
+
if (def.primaryKey && this.getPkFields().length === 1) parts.push("PRIMARY KEY");
|
|
1051
1129
|
if (def.autoIncrement) parts.push("AUTOINCREMENT");
|
|
1052
1130
|
if (def.required && !def.primaryKey) parts.push("NOT NULL");
|
|
1053
1131
|
// A callable default (e.g. `default: () => new Date()`) is resolved per-row
|
|
@@ -1061,6 +1139,15 @@ export class BaseModel {
|
|
|
1061
1139
|
colDefs.push(parts.join(" "));
|
|
1062
1140
|
}
|
|
1063
1141
|
|
|
1142
|
+
// A COMPOSITE key is declared ONCE, at table level; the per-column inline
|
|
1143
|
+
// form above is suppressed for it, because two inline primary keys is
|
|
1144
|
+
// invalid DDL on every engine.
|
|
1145
|
+
const pkFields = this.getPkFields();
|
|
1146
|
+
if (pkFields.length > 1) {
|
|
1147
|
+
const pkCols = pkFields.map((f) => this.getDbColumn(f));
|
|
1148
|
+
colDefs.push(`PRIMARY KEY (${pkCols.join(", ")})`);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1064
1151
|
const sql = `CREATE TABLE IF NOT EXISTS "${this.tableName}" (${colDefs.join(", ")})`;
|
|
1065
1152
|
await adapterStartTransaction(db);
|
|
1066
1153
|
try {
|
|
@@ -1099,7 +1186,7 @@ export class BaseModel {
|
|
|
1099
1186
|
* @param sql SQL query string.
|
|
1100
1187
|
* @param params Bind parameters.
|
|
1101
1188
|
* @param ttl Cache TTL in seconds (default 60).
|
|
1102
|
-
* @param limit Max records to return (default
|
|
1189
|
+
* @param limit Max records to return (default 100).
|
|
1103
1190
|
* @param offset Records to skip (default 0).
|
|
1104
1191
|
* @param include Relationship names to eager-load on cache miss.
|
|
1105
1192
|
*/
|
|
@@ -1108,7 +1195,7 @@ export class BaseModel {
|
|
|
1108
1195
|
sql: string,
|
|
1109
1196
|
params?: unknown[],
|
|
1110
1197
|
ttl = 60,
|
|
1111
|
-
limit =
|
|
1198
|
+
limit = DEFAULT_ROW_CAP,
|
|
1112
1199
|
offset = 0,
|
|
1113
1200
|
include?: string[],
|
|
1114
1201
|
): Promise<T[]> {
|
|
@@ -1117,7 +1204,7 @@ export class BaseModel {
|
|
|
1117
1204
|
ModelClass._queryCache = new QueryCache({ defaultTtl: ttl, maxSize: 500 });
|
|
1118
1205
|
}
|
|
1119
1206
|
const cacheKey = `${ModelClass.tableName}:${sql}:${limit}:${offset}`;
|
|
1120
|
-
const key = QueryCache.queryKey(cacheKey, params ?? []);
|
|
1207
|
+
const key = QueryCache.queryKey(cacheKey, params ?? [], ModelClass.getDb().cacheIdentity ?? "");
|
|
1121
1208
|
const hit = ModelClass._queryCache.get(key) as T[] | undefined;
|
|
1122
1209
|
if (hit !== undefined) return hit;
|
|
1123
1210
|
|
|
@@ -1149,10 +1236,18 @@ export class BaseModel {
|
|
|
1149
1236
|
this: new (data?: Record<string, unknown>) => T,
|
|
1150
1237
|
sql: string,
|
|
1151
1238
|
params?: unknown[],
|
|
1239
|
+
limit: number = DEFAULT_ROW_CAP,
|
|
1240
|
+
offset: number = 0,
|
|
1152
1241
|
): Promise<T[]> {
|
|
1153
1242
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
1154
1243
|
const db = ModelClass.getDb();
|
|
1155
|
-
|
|
1244
|
+
// Skip appending when the caller's SQL already carries its own LIMIT --
|
|
1245
|
+
// a second one is a syntax error on every engine. SQLTranslator.appendLimit
|
|
1246
|
+
// scrubs literals/comments first (a substring search here used to drop the
|
|
1247
|
+
// row cap on `WHERE label != 'LIMIT'`) and appends on a new line so the
|
|
1248
|
+
// clause is never swallowed by a trailing `--` comment.
|
|
1249
|
+
const paged = SQLTranslator.appendLimit(sql, limit, offset);
|
|
1250
|
+
const rows = await adapterQuery(db, paged, params);
|
|
1156
1251
|
return rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
1157
1252
|
}
|
|
1158
1253
|
|
|
@@ -1188,8 +1283,8 @@ export class BaseModel {
|
|
|
1188
1283
|
await adapterStartTransaction(db);
|
|
1189
1284
|
try {
|
|
1190
1285
|
await adapterExecute(db,
|
|
1191
|
-
`DELETE FROM "${ModelClass.tableName}" WHERE
|
|
1192
|
-
|
|
1286
|
+
`DELETE FROM "${ModelClass.tableName}" WHERE ${this.pkWhere().sql}`,
|
|
1287
|
+
this.pkWhere().params,
|
|
1193
1288
|
);
|
|
1194
1289
|
await adapterCommit(db);
|
|
1195
1290
|
} catch (e) {
|
|
@@ -1220,8 +1315,8 @@ export class BaseModel {
|
|
|
1220
1315
|
await adapterStartTransaction(db);
|
|
1221
1316
|
try {
|
|
1222
1317
|
await adapterExecute(db,
|
|
1223
|
-
`UPDATE "${ModelClass.tableName}" SET is_deleted = 0 WHERE
|
|
1224
|
-
|
|
1318
|
+
`UPDATE "${ModelClass.tableName}" SET is_deleted = 0 WHERE ${this.pkWhere().sql}`,
|
|
1319
|
+
this.pkWhere().params,
|
|
1225
1320
|
);
|
|
1226
1321
|
await adapterCommit(db);
|
|
1227
1322
|
} catch (e) {
|
|
@@ -1239,8 +1334,8 @@ export class BaseModel {
|
|
|
1239
1334
|
this: new (data?: Record<string, unknown>) => T,
|
|
1240
1335
|
conditions?: string,
|
|
1241
1336
|
params?: unknown[],
|
|
1242
|
-
limit
|
|
1243
|
-
offset
|
|
1337
|
+
limit: number = DEFAULT_ROW_CAP,
|
|
1338
|
+
offset: number = 0,
|
|
1244
1339
|
): Promise<T[]> {
|
|
1245
1340
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
1246
1341
|
const db = ModelClass.getDb();
|
|
@@ -1309,7 +1404,7 @@ export class BaseModel {
|
|
|
1309
1404
|
params?: unknown[],
|
|
1310
1405
|
): void {
|
|
1311
1406
|
const ModelClass = this as unknown as typeof BaseModel;
|
|
1312
|
-
(ModelClass as any)[name] = (limit: number =
|
|
1407
|
+
(ModelClass as any)[name] = (limit: number = DEFAULT_ROW_CAP, offset: number = 0) => {
|
|
1313
1408
|
return ModelClass.where.call(ModelClass as any, filterSql, params, limit, offset);
|
|
1314
1409
|
};
|
|
1315
1410
|
}
|