@zhin.js/database 1.1.7 → 1.1.8
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/README.md +16 -10
- package/lib/dialects/sqlite.d.ts +24 -0
- package/lib/dialects/sqlite.js +105 -9
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/probe.d.ts +30 -0
- package/lib/probe.js +98 -0
- package/lib/reserved-tables.d.ts +18 -0
- package/lib/reserved-tables.js +39 -0
- package/lib/types.d.ts +5 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Universal database abstraction layer for Zhin.js framework with support for mult
|
|
|
8
8
|
npm install @zhin.js/database
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Install the database driver you need(SQLite 使用 Node 内置 `node:sqlite`,无需安装,需 Node.js 22.
|
|
11
|
+
Install the database driver you need(SQLite 使用 Node 内置 `node:sqlite`,无需安装,需 Node.js >=22.19.0,推荐 24+):
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# For MySQL
|
|
@@ -83,7 +83,7 @@ const user = await userModel.create({
|
|
|
83
83
|
## Supported Databases
|
|
84
84
|
|
|
85
85
|
### ✅ Relational Databases (已完整实现)
|
|
86
|
-
- **SQLite** - 内置支持,使用 Node 自带 `node:sqlite`(无需安装驱动,需 Node.js 22.
|
|
86
|
+
- **SQLite** - 内置支持,使用 Node 自带 `node:sqlite`(无需安装驱动,需 Node.js >=22.19.0,推荐 24+)
|
|
87
87
|
- 轻量级、零配置
|
|
88
88
|
- 适合中小型应用
|
|
89
89
|
- 支持 WAL 模式
|
|
@@ -262,27 +262,33 @@ await db.start();
|
|
|
262
262
|
|
|
263
263
|
## Model API
|
|
264
264
|
|
|
265
|
+
以下为关系型模型(`RelatedModel`,SQLite / MySQL / PostgreSQL)的便捷方法;更复杂的查询见下方链式查询 API。
|
|
266
|
+
|
|
265
267
|
```typescript
|
|
266
268
|
const model = db.model('users');
|
|
267
269
|
|
|
268
270
|
// Create
|
|
269
271
|
const user = await model.create({ name: 'John', email: 'john@example.com' });
|
|
272
|
+
const created = await model.createMany([{ name: 'A' }, { name: 'B' }]);
|
|
270
273
|
|
|
271
274
|
// Read
|
|
272
|
-
const users = await model.
|
|
273
|
-
const
|
|
275
|
+
const users = await model.findAll({ name: 'John' });
|
|
276
|
+
const one = await model.findOne({ email: 'john@example.com' });
|
|
277
|
+
const byId = await model.findById(1);
|
|
274
278
|
|
|
275
|
-
// Update
|
|
276
|
-
await model.
|
|
279
|
+
// Update —— 条件在前、数据在后;返回受影响行数
|
|
280
|
+
await model.updateWhere({ name: 'John' }, { name: 'Jane' });
|
|
281
|
+
await model.updateById(1, { name: 'Jane' });
|
|
277
282
|
|
|
278
|
-
// Delete
|
|
279
|
-
await model.
|
|
283
|
+
// Delete —— 启用软删除的模型会改为标记 deletedAt
|
|
284
|
+
await model.deleteWhere({ name: 'Jane' });
|
|
285
|
+
await model.deleteById(1);
|
|
280
286
|
|
|
281
287
|
// Count
|
|
282
288
|
const count = await model.count({ email: { $like: '%@example.com' } });
|
|
283
289
|
|
|
284
|
-
// Pagination
|
|
285
|
-
const
|
|
290
|
+
// Pagination —— 用链式查询
|
|
291
|
+
const page = await model.select().where({}).limit(10).offset(0);
|
|
286
292
|
```
|
|
287
293
|
|
|
288
294
|
## 链式查询 API (Fluent Query Builder)
|
package/lib/dialects/sqlite.d.ts
CHANGED
|
@@ -4,10 +4,30 @@ import { RelatedDatabase } from '../type/related/database.js';
|
|
|
4
4
|
export interface SQLiteDialectConfig {
|
|
5
5
|
filename: string;
|
|
6
6
|
mode?: string;
|
|
7
|
+
/**
|
|
8
|
+
* `PRAGMA busy_timeout` in ms (default 0: no synchronous wait; see `busyRetryMs`). The wait
|
|
9
|
+
* is synchronous (`node:sqlite`), so only a connection whose own writes are short statements
|
|
10
|
+
* should set it — the gateway does for its `console_*` connection (ADR 0054 §7). Two
|
|
11
|
+
* connections of one process with async transactions would block each other's event loop.
|
|
12
|
+
*/
|
|
13
|
+
busyTimeoutMs?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Without `busyTimeoutMs`, a statement outside a transaction (and `BEGIN IMMEDIATE`) that hits
|
|
16
|
+
* `SQLITE_BUSY` is retried with an async backoff for at most this long (default 5000 ms; 0
|
|
17
|
+
* disables). The event loop keeps running while another process (the gateway) holds the
|
|
18
|
+
* write lock, so two connections in one process cannot deadlock each other.
|
|
19
|
+
*/
|
|
20
|
+
busyRetryMs?: number;
|
|
7
21
|
}
|
|
22
|
+
/** Default budget of {@link SQLiteDialectConfig.busyRetryMs}. */
|
|
23
|
+
export declare const SQLITE_BUSY_RETRY_MS = 5000;
|
|
24
|
+
/** `SQLITE_BUSY` and its extended codes except `SQLITE_BUSY_SNAPSHOT` (a stale read snapshot never clears by waiting). */
|
|
25
|
+
export declare function isSqliteBusyError(error: unknown): boolean;
|
|
8
26
|
export declare class SQLiteDialect<S extends Record<string, object> = Record<string, object>> extends Dialect<SQLiteDialectConfig, S, string> {
|
|
9
27
|
/** Node 内置 SQLite 连接(node:sqlite),在 connect() 中动态加载 */
|
|
10
28
|
private db;
|
|
29
|
+
/** Own transaction flag; `DatabaseSync#isTransaction` is preferred where Node provides it. */
|
|
30
|
+
private transactionOpen;
|
|
11
31
|
constructor(config: SQLiteDialectConfig);
|
|
12
32
|
isConnected(): boolean;
|
|
13
33
|
connect(): Promise<void>;
|
|
@@ -16,6 +36,10 @@ export declare class SQLiteDialect<S extends Record<string, object> = Record<str
|
|
|
16
36
|
/** node:sqlite 仅支持 number/string/bigint/buffer/null;JSON 列在库内为 TEXT,对象需序列化 */
|
|
17
37
|
private prepareBindParams;
|
|
18
38
|
query<U = any>(sql: string, params?: any[]): Promise<U>;
|
|
39
|
+
/** Async retry budget: only for connections that do not wait synchronously (`busyTimeoutMs`). */
|
|
40
|
+
private busyRetryBudget;
|
|
41
|
+
private inTransaction;
|
|
42
|
+
private execute;
|
|
19
43
|
/** Column declarations are scoped to the table in this database, never the result alias. */
|
|
20
44
|
private readonly jsonColumns;
|
|
21
45
|
registerTableSchema(table: string, definition: Record<string, {
|
package/lib/dialects/sqlite.js
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
4
|
import { Dialect } from '../base/index.js';
|
|
4
5
|
import { RelatedDatabase } from '../type/related/database.js';
|
|
6
|
+
/** Default budget of {@link SQLiteDialectConfig.busyRetryMs}. */
|
|
7
|
+
export const SQLITE_BUSY_RETRY_MS = 5_000;
|
|
8
|
+
const BUSY_RETRY_FIRST_DELAY_MS = 2;
|
|
9
|
+
const BUSY_RETRY_MAX_DELAY_MS = 100;
|
|
10
|
+
/** `SQLITE_BUSY` and its extended codes except `SQLITE_BUSY_SNAPSHOT` (a stale read snapshot never clears by waiting). */
|
|
11
|
+
export function isSqliteBusyError(error) {
|
|
12
|
+
if (!error || typeof error !== 'object')
|
|
13
|
+
return false;
|
|
14
|
+
const errcode = error.errcode;
|
|
15
|
+
return typeof errcode === 'number' && (errcode & 0xff) === 5 && errcode !== 517;
|
|
16
|
+
}
|
|
17
|
+
function isInMemoryFilename(filename) {
|
|
18
|
+
return filename === ':memory:' || filename === '' || filename.startsWith('file::memory:');
|
|
19
|
+
}
|
|
5
20
|
export class SQLiteDialect extends Dialect {
|
|
6
21
|
/** Node 内置 SQLite 连接(node:sqlite),在 connect() 中动态加载 */
|
|
7
22
|
db = null;
|
|
23
|
+
/** Own transaction flag; `DatabaseSync#isTransaction` is preferred where Node provides it. */
|
|
24
|
+
transactionOpen = false;
|
|
8
25
|
constructor(config) {
|
|
9
26
|
super('sqlite', config);
|
|
10
27
|
}
|
|
@@ -20,7 +37,21 @@ export class SQLiteDialect extends Dialect {
|
|
|
20
37
|
if (!fs.existsSync(dirname)) {
|
|
21
38
|
fs.mkdirSync(dirname, { recursive: true });
|
|
22
39
|
}
|
|
23
|
-
|
|
40
|
+
const db = new DatabaseSync(this.config.filename);
|
|
41
|
+
try {
|
|
42
|
+
// Two processes share the file (gateway: console_* tables, instance: the rest):
|
|
43
|
+
// WAL lets readers run beside one writer and never blocks readers on a writer.
|
|
44
|
+
const busy = this.config.busyTimeoutMs;
|
|
45
|
+
if (busy !== undefined && Number.isSafeInteger(busy) && busy > 0)
|
|
46
|
+
db.exec(`PRAGMA busy_timeout = ${busy}`);
|
|
47
|
+
if (!isInMemoryFilename(this.config.filename))
|
|
48
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
db.close();
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
this.db = db;
|
|
24
55
|
}
|
|
25
56
|
catch (error) {
|
|
26
57
|
throw new Error(`SQLite 连接失败(需要 Node.js 22.5+,推荐 24+): ${error}`);
|
|
@@ -58,6 +89,42 @@ export class SQLiteDialect extends Dialect {
|
|
|
58
89
|
});
|
|
59
90
|
}
|
|
60
91
|
async query(sql, params) {
|
|
92
|
+
const budget = this.busyRetryBudget();
|
|
93
|
+
if (budget <= 0)
|
|
94
|
+
return this.execute(sql, params);
|
|
95
|
+
const deadline = Date.now() + budget;
|
|
96
|
+
let wait = BUSY_RETRY_FIRST_DELAY_MS;
|
|
97
|
+
for (;;) {
|
|
98
|
+
// A statement inside an open transaction is never replayed here: the transaction's caller
|
|
99
|
+
// owns that retry (BEGIN IMMEDIATE takes the write lock up front, so this is rare).
|
|
100
|
+
// COMMIT is the exception: SQLite documents a busy COMMIT as safe to retry.
|
|
101
|
+
const replayable = !this.inTransaction() || /^\s*commit\b/i.test(sql);
|
|
102
|
+
try {
|
|
103
|
+
return await this.execute(sql, params);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (!replayable || !isSqliteBusyError(error) || Date.now() + wait > deadline)
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
await delay(wait);
|
|
110
|
+
wait = Math.min(wait * 2, BUSY_RETRY_MAX_DELAY_MS);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Async retry budget: only for connections that do not wait synchronously (`busyTimeoutMs`). */
|
|
114
|
+
busyRetryBudget() {
|
|
115
|
+
const busy = this.config.busyTimeoutMs;
|
|
116
|
+
if (busy !== undefined && busy > 0)
|
|
117
|
+
return 0;
|
|
118
|
+
if (isInMemoryFilename(this.config.filename))
|
|
119
|
+
return 0;
|
|
120
|
+
const budget = this.config.busyRetryMs ?? SQLITE_BUSY_RETRY_MS;
|
|
121
|
+
return Number.isFinite(budget) && budget > 0 ? budget : 0;
|
|
122
|
+
}
|
|
123
|
+
inTransaction() {
|
|
124
|
+
const native = this.db?.isTransaction;
|
|
125
|
+
return typeof native === 'boolean' ? native : this.transactionOpen;
|
|
126
|
+
}
|
|
127
|
+
async execute(sql, params) {
|
|
61
128
|
if (!this.db)
|
|
62
129
|
throw new Error('SQLite 未连接');
|
|
63
130
|
const trimmedSql = sql.trim().toLowerCase();
|
|
@@ -235,15 +302,44 @@ export class SQLiteDialect extends Dialect {
|
|
|
235
302
|
*/
|
|
236
303
|
async beginTransaction(options) {
|
|
237
304
|
const dialect = this;
|
|
238
|
-
|
|
239
|
-
|
|
305
|
+
const readOnly = options?.readOnly === true;
|
|
306
|
+
// Write transactions: IMMEDIATE takes the write lock at BEGIN, so contention with the
|
|
307
|
+
// gateway's connection surfaces there (retried by query()), never as SQLITE_BUSY_SNAPSHOT
|
|
308
|
+
// halfway through. A declared read-only transaction takes no write lock (DEFERRED: WAL
|
|
309
|
+
// readers never wait for a writer) and query_only makes an accidental write fail loudly.
|
|
310
|
+
// Callers that cannot declare intent (the plugin-runtime Database Host) stay IMMEDIATE.
|
|
311
|
+
await this.query(readOnly ? 'BEGIN DEFERRED' : 'BEGIN IMMEDIATE');
|
|
312
|
+
this.transactionOpen = true;
|
|
313
|
+
if (readOnly) {
|
|
314
|
+
try {
|
|
315
|
+
await this.query('PRAGMA query_only = ON');
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
await this.query('ROLLBACK').catch(() => undefined);
|
|
319
|
+
this.transactionOpen = false;
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const close = async () => {
|
|
324
|
+
dialect.transactionOpen = false;
|
|
325
|
+
if (readOnly)
|
|
326
|
+
await dialect.query('PRAGMA query_only = OFF').catch(() => undefined);
|
|
327
|
+
};
|
|
328
|
+
const finish = async (sql) => {
|
|
329
|
+
try {
|
|
330
|
+
await dialect.query(sql);
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
// A failed COMMIT (e.g. busy past the budget) leaves the transaction to the caller's rollback.
|
|
334
|
+
if (sql === 'ROLLBACK')
|
|
335
|
+
await close();
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
await close();
|
|
339
|
+
};
|
|
240
340
|
return {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
},
|
|
244
|
-
async rollback() {
|
|
245
|
-
await dialect.query('ROLLBACK');
|
|
246
|
-
},
|
|
341
|
+
commit: () => finish('COMMIT'),
|
|
342
|
+
rollback: () => finish('ROLLBACK'),
|
|
247
343
|
async query(sql, params) {
|
|
248
344
|
return dialect.query(sql, params);
|
|
249
345
|
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -13,5 +13,7 @@ export * from './dialects/memory.js';
|
|
|
13
13
|
export * from './dialects/sqlite.js';
|
|
14
14
|
export * from './migration.js';
|
|
15
15
|
export * from './structured-schema.js';
|
|
16
|
+
export * from './probe.js';
|
|
17
|
+
export * from './reserved-tables.js';
|
|
16
18
|
import { registerBuiltinDatabases } from './builtin-runtime.js';
|
|
17
19
|
registerBuiltinDatabases();
|
package/lib/probe.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot connection probe for setup wizards: connect, run the dialect health query, disconnect.
|
|
3
|
+
* Error text is stripped of credentials before it is returned.
|
|
4
|
+
*/
|
|
5
|
+
export type ProbeDialect = 'sqlite' | 'mysql' | 'pg' | 'mongodb';
|
|
6
|
+
export interface DatabaseProbeResult {
|
|
7
|
+
readonly ok: boolean;
|
|
8
|
+
readonly latencyMs?: number;
|
|
9
|
+
readonly error?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ProbeTarget {
|
|
12
|
+
connect(): Promise<void>;
|
|
13
|
+
disconnect(): Promise<void>;
|
|
14
|
+
healthCheck(): Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
/** Driver package that must be importable for each dialect (`null`: built into Node). */
|
|
17
|
+
export declare const DATABASE_DRIVER_PACKAGES: Readonly<Record<ProbeDialect, string | null>>;
|
|
18
|
+
/** Whether the optional driver package for `dialect` can be loaded. */
|
|
19
|
+
export declare function isDatabaseDriverAvailable(dialect: ProbeDialect, importer?: (specifier: string) => Promise<unknown>): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Opens a temporary connection with `config` and closes it again.
|
|
22
|
+
* `factory` is a test seam; it defaults to the built-in dialects.
|
|
23
|
+
*/
|
|
24
|
+
export declare function probeDatabaseConnection(dialect: ProbeDialect, config: Record<string, unknown>, options?: Readonly<{
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
factory?: (dialect: ProbeDialect, config: Record<string, unknown>) => Promise<ProbeTarget>;
|
|
27
|
+
}>): Promise<DatabaseProbeResult>;
|
|
28
|
+
/** Removes credentials (configured secret values, URL userinfo) from a driver error. */
|
|
29
|
+
export declare function redactProbeError(error: unknown, secrets?: readonly string[]): string;
|
|
30
|
+
export {};
|
package/lib/probe.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot connection probe for setup wizards: connect, run the dialect health query, disconnect.
|
|
3
|
+
* Error text is stripped of credentials before it is returned.
|
|
4
|
+
*/
|
|
5
|
+
/** Driver package that must be importable for each dialect (`null`: built into Node). */
|
|
6
|
+
export const DATABASE_DRIVER_PACKAGES = Object.freeze({
|
|
7
|
+
sqlite: null,
|
|
8
|
+
mysql: 'mysql2',
|
|
9
|
+
pg: 'pg',
|
|
10
|
+
mongodb: 'mongodb',
|
|
11
|
+
});
|
|
12
|
+
/** Whether the optional driver package for `dialect` can be loaded. */
|
|
13
|
+
export async function isDatabaseDriverAvailable(dialect, importer = specifier => import(specifier)) {
|
|
14
|
+
const pkg = DATABASE_DRIVER_PACKAGES[dialect];
|
|
15
|
+
if (pkg === null)
|
|
16
|
+
return true;
|
|
17
|
+
try {
|
|
18
|
+
await importer(pkg);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function createProbeTarget(dialect, config) {
|
|
26
|
+
switch (dialect) {
|
|
27
|
+
case 'sqlite': {
|
|
28
|
+
const { SQLiteDialect } = await import('./dialects/sqlite.js');
|
|
29
|
+
return new SQLiteDialect(config);
|
|
30
|
+
}
|
|
31
|
+
case 'mysql': {
|
|
32
|
+
const { MySQLDialect } = await import('./dialects/mysql.js');
|
|
33
|
+
return new MySQLDialect(config);
|
|
34
|
+
}
|
|
35
|
+
case 'pg': {
|
|
36
|
+
const { PostgreSQLDialect } = await import('./dialects/pg.js');
|
|
37
|
+
return new PostgreSQLDialect(config);
|
|
38
|
+
}
|
|
39
|
+
case 'mongodb': {
|
|
40
|
+
const { MongoDBDialect } = await import('./dialects/mongodb.js');
|
|
41
|
+
return new MongoDBDialect(config);
|
|
42
|
+
}
|
|
43
|
+
default:
|
|
44
|
+
throw new Error(`Unsupported dialect: ${String(dialect)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Opens a temporary connection with `config` and closes it again.
|
|
49
|
+
* `factory` is a test seam; it defaults to the built-in dialects.
|
|
50
|
+
*/
|
|
51
|
+
export async function probeDatabaseConnection(dialect, config, options = {}) {
|
|
52
|
+
const secrets = collectSecrets(config);
|
|
53
|
+
const startedAt = Date.now();
|
|
54
|
+
let target;
|
|
55
|
+
let timer;
|
|
56
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
57
|
+
timer = setTimeout(() => reject(new Error('connection timed out')), options.timeoutMs ?? 10_000);
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
target = await (options.factory ?? createProbeTarget)(dialect, config);
|
|
61
|
+
await Promise.race([target.connect(), timeout]);
|
|
62
|
+
const healthy = await Promise.race([target.healthCheck(), timeout]);
|
|
63
|
+
if (!healthy)
|
|
64
|
+
return Object.freeze({ ok: false, error: 'probe query failed' });
|
|
65
|
+
return Object.freeze({ ok: true, latencyMs: Date.now() - startedAt });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return Object.freeze({ ok: false, error: redactProbeError(error, secrets) });
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
await target?.disconnect().catch(() => undefined);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function collectSecrets(config) {
|
|
76
|
+
const values = [];
|
|
77
|
+
for (const [key, value] of Object.entries(config)) {
|
|
78
|
+
if (typeof value !== 'string' || !value)
|
|
79
|
+
continue;
|
|
80
|
+
if (/pass|secret|token|key/iu.test(key))
|
|
81
|
+
values.push(value);
|
|
82
|
+
if (/url|uri|connectionString/iu.test(key)) {
|
|
83
|
+
const credentials = /\/\/([^@/]*)@/u.exec(value)?.[1];
|
|
84
|
+
if (credentials)
|
|
85
|
+
values.push(credentials, ...credentials.split(':').filter(Boolean));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return values.filter(value => value.length >= 3).sort((a, b) => b.length - a.length);
|
|
89
|
+
}
|
|
90
|
+
/** Removes credentials (configured secret values, URL userinfo) from a driver error. */
|
|
91
|
+
export function redactProbeError(error, secrets = []) {
|
|
92
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
for (const secret of secrets)
|
|
94
|
+
message = message.split(secret).join('***');
|
|
95
|
+
message = message.replace(/\/\/[^@\s/]*@/gu, '//***@');
|
|
96
|
+
message = message.replace(/(password|pwd)\s*[=:]\s*\S+/giu, '$1=***');
|
|
97
|
+
return message.slice(0, 300);
|
|
98
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tables owned by the management gateway (ADR 0049 / 0054 §7). The gateway and the instance share
|
|
3
|
+
* one project database; the gateway's tables hold password hashes, session / token hashes and the
|
|
4
|
+
* audit log, so every instance-side path (plugin models, Console database page, raw SQL) must deny
|
|
5
|
+
* them. Deny by prefix so future gateway tables are covered without another list to update.
|
|
6
|
+
*/
|
|
7
|
+
export declare const GATEWAY_TABLE_PREFIX = "console_";
|
|
8
|
+
/** `console_`-prefixed tables that the instance itself owns (historical names). */
|
|
9
|
+
export declare const INSTANCE_CONSOLE_TABLES: readonly string[];
|
|
10
|
+
/** True when `name` is a gateway-owned table (case-insensitive, quotes and schema ignored). */
|
|
11
|
+
export declare function isGatewayOwnedTable(name: string): boolean;
|
|
12
|
+
/** True when SQL text mentions a gateway-owned table anywhere (identifiers, quoted or literal). */
|
|
13
|
+
export declare function referencesGatewayOwnedTable(sql: string): boolean;
|
|
14
|
+
export declare class GatewayOwnedTableError extends Error {
|
|
15
|
+
constructor(table?: string);
|
|
16
|
+
}
|
|
17
|
+
/** Throws {@link GatewayOwnedTableError} for a gateway-owned table name. */
|
|
18
|
+
export declare function assertInstanceTable(name: string): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tables owned by the management gateway (ADR 0049 / 0054 §7). The gateway and the instance share
|
|
3
|
+
* one project database; the gateway's tables hold password hashes, session / token hashes and the
|
|
4
|
+
* audit log, so every instance-side path (plugin models, Console database page, raw SQL) must deny
|
|
5
|
+
* them. Deny by prefix so future gateway tables are covered without another list to update.
|
|
6
|
+
*/
|
|
7
|
+
export const GATEWAY_TABLE_PREFIX = 'console_';
|
|
8
|
+
/** `console_`-prefixed tables that the instance itself owns (historical names). */
|
|
9
|
+
export const INSTANCE_CONSOLE_TABLES = Object.freeze(['console_conversations']);
|
|
10
|
+
const instanceConsoleTables = new Set(INSTANCE_CONSOLE_TABLES);
|
|
11
|
+
/** True when `name` is a gateway-owned table (case-insensitive, quotes and schema ignored). */
|
|
12
|
+
export function isGatewayOwnedTable(name) {
|
|
13
|
+
const bare = String(name).trim().replace(/^[`"[]|[`"\]]$/g, '');
|
|
14
|
+
const table = (bare.includes('.') ? bare.slice(bare.lastIndexOf('.') + 1) : bare)
|
|
15
|
+
.replace(/^[`"[]|[`"\]]$/g, '').toLowerCase();
|
|
16
|
+
return table.startsWith(GATEWAY_TABLE_PREFIX) && !instanceConsoleTables.has(table);
|
|
17
|
+
}
|
|
18
|
+
const IDENTIFIER = /(?<![A-Za-z0-9_$])console_[A-Za-z0-9_$]*/gi;
|
|
19
|
+
/** True when SQL text mentions a gateway-owned table anywhere (identifiers, quoted or literal). */
|
|
20
|
+
export function referencesGatewayOwnedTable(sql) {
|
|
21
|
+
for (const match of String(sql).matchAll(IDENTIFIER)) {
|
|
22
|
+
if (isGatewayOwnedTable(match[0]))
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
export class GatewayOwnedTableError extends Error {
|
|
28
|
+
constructor(table) {
|
|
29
|
+
super(table
|
|
30
|
+
? `Table '${table}' is owned by the management gateway and is not accessible from the instance`
|
|
31
|
+
: 'Statement references a table owned by the management gateway');
|
|
32
|
+
this.name = 'GatewayOwnedTableError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Throws {@link GatewayOwnedTableError} for a gateway-owned table name. */
|
|
36
|
+
export function assertInstanceTable(name) {
|
|
37
|
+
if (isGatewayOwnedTable(name))
|
|
38
|
+
throw new GatewayOwnedTableError(name);
|
|
39
|
+
}
|
package/lib/types.d.ts
CHANGED
|
@@ -285,6 +285,11 @@ export type IsolationLevel = 'READ_UNCOMMITTED' | 'READ_COMMITTED' | 'REPEATABLE
|
|
|
285
285
|
export interface TransactionOptions {
|
|
286
286
|
isolationLevel?: IsolationLevel;
|
|
287
287
|
timeout?: number;
|
|
288
|
+
/**
|
|
289
|
+
* The transaction only reads. SQLite then opens it with `BEGIN DEFERRED` (no write lock, so it
|
|
290
|
+
* never waits for another connection's writer) and refuses writes inside it (`query_only`).
|
|
291
|
+
*/
|
|
292
|
+
readOnly?: boolean;
|
|
288
293
|
}
|
|
289
294
|
export interface Transaction {
|
|
290
295
|
commit(): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/database",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "Universal database abstraction layer for Zhin.js - supports SQLite, MySQL, PostgreSQL, MongoDB, Redis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -43,14 +43,15 @@
|
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=22.19.0"
|
|
45
45
|
},
|
|
46
|
+
"license": "MIT",
|
|
46
47
|
"dependencies": {
|
|
47
|
-
"@zhin.js/logger": "1.1.
|
|
48
|
+
"@zhin.js/logger": "1.1.3"
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
50
51
|
"mongodb": ">=3.1.13",
|
|
51
52
|
"mysql2": ">=3.9.8",
|
|
52
53
|
"pg": ">=2.11.2",
|
|
53
|
-
"redis": "
|
|
54
|
+
"redis": ">=4.0.0"
|
|
54
55
|
},
|
|
55
56
|
"peerDependenciesMeta": {
|
|
56
57
|
"mysql2": {
|