@zhin.js/database 1.0.77 → 1.0.79
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/lib/base/database.d.ts +5 -1
- package/lib/base/database.js +15 -1
- package/lib/dialects/sqlite.d.ts +11 -2
- package/lib/dialects/sqlite.js +40 -50
- package/lib/migration.js +34 -14
- package/lib/type/related/database.js +9 -1
- package/lib/type/related/model.js +5 -3
- package/package.json +4 -4
package/lib/base/database.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export declare abstract class Database<D = any, S extends Record<string, object>
|
|
|
23
23
|
private _logger?;
|
|
24
24
|
private _logging;
|
|
25
25
|
constructor(dialect: Dialect<D, S, Q>, definitions?: Database.DefinitionObj<S>);
|
|
26
|
+
/**
|
|
27
|
+
* 若方言支持按列声明类型处理(如 SQLite 读路径的 json 反序列化),向其注册表结构
|
|
28
|
+
*/
|
|
29
|
+
private registerDialectSchema;
|
|
26
30
|
/**
|
|
27
31
|
* 启用查询日志
|
|
28
32
|
* @param handler 自定义日志处理器,不传则使用默认控制台输出
|
|
@@ -123,6 +127,6 @@ export declare namespace Database {
|
|
|
123
127
|
get<K extends keyof S>(key: K): Model<D, S, Q, K> | undefined;
|
|
124
128
|
}
|
|
125
129
|
type DefinitionObj<S extends Record<string, object>> = {
|
|
126
|
-
[K in keyof S]
|
|
130
|
+
[K in keyof S]?: Definition<S[K]>;
|
|
127
131
|
};
|
|
128
132
|
}
|
package/lib/base/database.js
CHANGED
|
@@ -24,7 +24,20 @@ export class Database {
|
|
|
24
24
|
constructor(dialect, definitions) {
|
|
25
25
|
this.dialect = dialect;
|
|
26
26
|
for (const key in definitions) {
|
|
27
|
-
|
|
27
|
+
const definition = definitions[key];
|
|
28
|
+
if (!definition)
|
|
29
|
+
continue;
|
|
30
|
+
this.definitions.set(key, definition);
|
|
31
|
+
this.registerDialectSchema(key, definition);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 若方言支持按列声明类型处理(如 SQLite 读路径的 json 反序列化),向其注册表结构
|
|
36
|
+
*/
|
|
37
|
+
registerDialectSchema(name, definition) {
|
|
38
|
+
const d = this.dialect;
|
|
39
|
+
if (typeof d.registerTableSchema === 'function') {
|
|
40
|
+
d.registerTableSchema(String(name), definition);
|
|
28
41
|
}
|
|
29
42
|
}
|
|
30
43
|
// ============================================================================
|
|
@@ -126,6 +139,7 @@ export class Database {
|
|
|
126
139
|
}
|
|
127
140
|
define(name, definition) {
|
|
128
141
|
this.definitions.set(name, definition);
|
|
142
|
+
this.registerDialectSchema(name, definition);
|
|
129
143
|
}
|
|
130
144
|
/**
|
|
131
145
|
* 删除表定义
|
package/lib/dialects/sqlite.d.ts
CHANGED
|
@@ -16,8 +16,16 @@ export declare class SQLiteDialect<S extends Record<string, object> = Record<str
|
|
|
16
16
|
/** node:sqlite 仅支持 number/string/bigint/buffer/null;JSON 列在库内为 TEXT,对象需序列化 */
|
|
17
17
|
private prepareBindParams;
|
|
18
18
|
query<U = any>(sql: string, params?: any[]): Promise<U>;
|
|
19
|
+
/** 声明为 json 的列名集合(由 Database 层注册表结构时填充),读路径仅对这些列做 JSON 反序列化 */
|
|
20
|
+
private readonly jsonColumns;
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
22
|
+
* 注册表的列声明类型,用于读路径按声明类型反序列化(避免对 TEXT 列做启发式 JSON 解析破坏往返)
|
|
23
|
+
*/
|
|
24
|
+
registerTableSchema(table: string, definition: Record<string, {
|
|
25
|
+
type: string;
|
|
26
|
+
}>): void;
|
|
27
|
+
/**
|
|
28
|
+
* 处理查询结果,按列声明类型反序列化(json 列)
|
|
21
29
|
*/
|
|
22
30
|
private processQueryResults;
|
|
23
31
|
/**
|
|
@@ -25,7 +33,8 @@ export declare class SQLiteDialect<S extends Record<string, object> = Record<str
|
|
|
25
33
|
*/
|
|
26
34
|
private processRowData;
|
|
27
35
|
/**
|
|
28
|
-
*
|
|
36
|
+
* 处理字段值:仅对声明为 json 的列做 JSON 反序列化;
|
|
37
|
+
* 普通 TEXT 列原样返回,保证写入什么读出什么(不剥离引号、不启发式 JSON.parse)
|
|
29
38
|
*/
|
|
30
39
|
private processFieldValue;
|
|
31
40
|
dispose(): Promise<void>;
|
package/lib/dialects/sqlite.js
CHANGED
|
@@ -62,36 +62,42 @@ export class SQLiteDialect extends Dialect {
|
|
|
62
62
|
if (!this.db)
|
|
63
63
|
throw new Error('SQLite 未连接');
|
|
64
64
|
const trimmedSql = sql.trim().toLowerCase();
|
|
65
|
-
const isSelect = trimmedSql.startsWith('select');
|
|
66
65
|
const isInsert = trimmedSql.startsWith('insert');
|
|
67
66
|
const isUpdateOrDelete = trimmedSql.startsWith('update') || trimmedSql.startsWith('delete');
|
|
67
|
+
// 带 RETURNING 的 DML 有结果集,不能走 run 丢行
|
|
68
|
+
const hasReturning = /\breturning\b/.test(trimmedSql);
|
|
68
69
|
const args = this.prepareBindParams(params ?? []);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const result = stmt.run(...args);
|
|
77
|
-
return {
|
|
78
|
-
lastID: Number(result.lastInsertRowid),
|
|
79
|
-
changes: Number(result.changes),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
if (isUpdateOrDelete) {
|
|
83
|
-
const result = stmt.run(...args);
|
|
84
|
-
return Number(result.changes);
|
|
85
|
-
}
|
|
86
|
-
stmt.run(...args);
|
|
87
|
-
return undefined;
|
|
70
|
+
const stmt = this.db.prepare(sql);
|
|
71
|
+
if (isInsert && !hasReturning) {
|
|
72
|
+
const result = stmt.run(...args);
|
|
73
|
+
return {
|
|
74
|
+
lastID: Number(result.lastInsertRowid),
|
|
75
|
+
changes: Number(result.changes),
|
|
76
|
+
};
|
|
88
77
|
}
|
|
89
|
-
|
|
90
|
-
|
|
78
|
+
if (isUpdateOrDelete && !hasReturning) {
|
|
79
|
+
const result = stmt.run(...args);
|
|
80
|
+
return Number(result.changes);
|
|
81
|
+
}
|
|
82
|
+
// SELECT / WITH / PRAGMA / DDL / 带 RETURNING 的 DML:按可能有结果集处理。
|
|
83
|
+
// node:sqlite 的 all() 对无结果集语句返回空数组,不会抛错。
|
|
84
|
+
const rows = stmt.all(...args);
|
|
85
|
+
return this.processQueryResults(rows);
|
|
86
|
+
}
|
|
87
|
+
/** 声明为 json 的列名集合(由 Database 层注册表结构时填充),读路径仅对这些列做 JSON 反序列化 */
|
|
88
|
+
jsonColumns = new Set();
|
|
89
|
+
/**
|
|
90
|
+
* 注册表的列声明类型,用于读路径按声明类型反序列化(避免对 TEXT 列做启发式 JSON 解析破坏往返)
|
|
91
|
+
*/
|
|
92
|
+
registerTableSchema(table, definition) {
|
|
93
|
+
for (const [columnName, column] of Object.entries(definition)) {
|
|
94
|
+
if (column && column.type === 'json') {
|
|
95
|
+
this.jsonColumns.add(columnName);
|
|
96
|
+
}
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
99
|
/**
|
|
94
|
-
*
|
|
100
|
+
* 处理查询结果,按列声明类型反序列化(json 列)
|
|
95
101
|
*/
|
|
96
102
|
processQueryResults(data) {
|
|
97
103
|
if (!data)
|
|
@@ -112,42 +118,26 @@ export class SQLiteDialect extends Dialect {
|
|
|
112
118
|
return row;
|
|
113
119
|
const processedRow = {};
|
|
114
120
|
for (const [key, value] of Object.entries(row)) {
|
|
115
|
-
processedRow[key] = this.processFieldValue(value);
|
|
121
|
+
processedRow[key] = this.processFieldValue(key, value);
|
|
116
122
|
}
|
|
117
123
|
return processedRow;
|
|
118
124
|
}
|
|
119
125
|
/**
|
|
120
|
-
*
|
|
126
|
+
* 处理字段值:仅对声明为 json 的列做 JSON 反序列化;
|
|
127
|
+
* 普通 TEXT 列原样返回,保证写入什么读出什么(不剥离引号、不启发式 JSON.parse)
|
|
121
128
|
*/
|
|
122
|
-
processFieldValue(value) {
|
|
129
|
+
processFieldValue(key, value) {
|
|
123
130
|
if (typeof value !== 'string')
|
|
124
131
|
return value;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// 尝试解析为 JSON(用于 json 类型字段)
|
|
130
|
-
if (unquoted.startsWith('{') || unquoted.startsWith('[')) {
|
|
131
|
-
try {
|
|
132
|
-
return JSON.parse(unquoted);
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// 如果解析失败,返回去除引号的字符串
|
|
136
|
-
return unquoted;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return unquoted;
|
|
132
|
+
if (!this.jsonColumns.has(key))
|
|
133
|
+
return value;
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(value);
|
|
140
136
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return JSON.parse(value);
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
// 解析失败,返回原始字符串
|
|
148
|
-
}
|
|
137
|
+
catch {
|
|
138
|
+
// 解析失败,返回原始字符串
|
|
139
|
+
return value;
|
|
149
140
|
}
|
|
150
|
-
return value;
|
|
151
141
|
}
|
|
152
142
|
async dispose() {
|
|
153
143
|
if (this.db) {
|
package/lib/migration.js
CHANGED
|
@@ -4,49 +4,65 @@
|
|
|
4
4
|
*/
|
|
5
5
|
class RecordingMigrationContext {
|
|
6
6
|
baseContext;
|
|
7
|
+
execute;
|
|
7
8
|
_operations = [];
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* @param baseContext 实际执行的上下文
|
|
11
|
+
* @param execute 是否真正执行操作;false 时为 dry-run,仅记录操作不触碰数据库
|
|
12
|
+
*/
|
|
13
|
+
constructor(baseContext, execute = true) {
|
|
9
14
|
this.baseContext = baseContext;
|
|
15
|
+
this.execute = execute;
|
|
10
16
|
}
|
|
11
17
|
get operations() {
|
|
12
18
|
return this._operations;
|
|
13
19
|
}
|
|
14
20
|
async createTable(tableName, columns) {
|
|
15
21
|
this._operations.push({ type: 'createTable', tableName, columns });
|
|
16
|
-
|
|
22
|
+
if (this.execute)
|
|
23
|
+
await this.baseContext.createTable(tableName, columns);
|
|
17
24
|
}
|
|
18
25
|
async dropTable(tableName) {
|
|
19
26
|
this._operations.push({ type: 'dropTable', tableName });
|
|
20
|
-
|
|
27
|
+
if (this.execute)
|
|
28
|
+
await this.baseContext.dropTable(tableName);
|
|
21
29
|
}
|
|
22
30
|
async addColumn(tableName, columnName, column) {
|
|
23
31
|
this._operations.push({ type: 'addColumn', tableName, columnName, column });
|
|
24
|
-
|
|
32
|
+
if (this.execute)
|
|
33
|
+
await this.baseContext.addColumn(tableName, columnName, column);
|
|
25
34
|
}
|
|
26
35
|
async dropColumn(tableName, columnName) {
|
|
27
36
|
this._operations.push({ type: 'dropColumn', tableName, columnName });
|
|
28
|
-
|
|
37
|
+
if (this.execute)
|
|
38
|
+
await this.baseContext.dropColumn(tableName, columnName);
|
|
29
39
|
}
|
|
30
40
|
async modifyColumn(tableName, columnName, column) {
|
|
31
41
|
// modifyColumn 不能自动反向,需要原始列定义
|
|
32
|
-
|
|
42
|
+
if (this.execute)
|
|
43
|
+
await this.baseContext.modifyColumn(tableName, columnName, column);
|
|
33
44
|
}
|
|
34
45
|
async renameColumn(tableName, oldName, newName) {
|
|
35
46
|
this._operations.push({ type: 'renameColumn', tableName, oldName, newName });
|
|
36
|
-
|
|
47
|
+
if (this.execute)
|
|
48
|
+
await this.baseContext.renameColumn(tableName, oldName, newName);
|
|
37
49
|
}
|
|
38
50
|
async addIndex(tableName, indexName, columns, unique) {
|
|
39
51
|
this._operations.push({ type: 'addIndex', tableName, indexName, columns, unique });
|
|
40
|
-
|
|
52
|
+
if (this.execute)
|
|
53
|
+
await this.baseContext.addIndex(tableName, indexName, columns, unique);
|
|
41
54
|
}
|
|
42
55
|
async dropIndex(tableName, indexName) {
|
|
43
56
|
this._operations.push({ type: 'dropIndex', tableName, indexName });
|
|
44
|
-
|
|
57
|
+
if (this.execute)
|
|
58
|
+
await this.baseContext.dropIndex(tableName, indexName);
|
|
45
59
|
}
|
|
46
60
|
async query(sql, params) {
|
|
47
61
|
// query 操作记录但无法自动反向
|
|
48
62
|
this._operations.push({ type: 'query', sql, params });
|
|
49
|
-
|
|
63
|
+
if (this.execute)
|
|
64
|
+
return this.baseContext.query(sql, params);
|
|
65
|
+
return undefined;
|
|
50
66
|
}
|
|
51
67
|
}
|
|
52
68
|
/**
|
|
@@ -333,12 +349,15 @@ export class MigrationRunner {
|
|
|
333
349
|
const migrated = [];
|
|
334
350
|
for (const migration of pending) {
|
|
335
351
|
try {
|
|
336
|
-
// 如果没有显式的 down
|
|
352
|
+
// 如果没有显式的 down,先 dry-run 记录操作并验证可以自动反向,通过后再真正执行
|
|
337
353
|
if (!migration.down) {
|
|
354
|
+
const dryRunContext = new RecordingMigrationContext(baseContext, false);
|
|
355
|
+
await migration.up(dryRunContext);
|
|
356
|
+
// 提前校验操作可反向(不可反向时抛错,此时数据库尚未被修改)
|
|
357
|
+
generateReverseOperations(dryRunContext.operations);
|
|
358
|
+
// 校验通过,真正执行并记录操作
|
|
338
359
|
const recordingContext = new RecordingMigrationContext(baseContext);
|
|
339
360
|
await migration.up(recordingContext);
|
|
340
|
-
// 验证操作可以反向(提前检查)
|
|
341
|
-
generateReverseOperations(recordingContext.operations);
|
|
342
361
|
await this.recordMigration(migration.name, batch, recordingContext.operations);
|
|
343
362
|
}
|
|
344
363
|
else {
|
|
@@ -438,8 +457,9 @@ function mapColumnType(type) {
|
|
|
438
457
|
function formatDefault(value) {
|
|
439
458
|
if (value === null)
|
|
440
459
|
return 'NULL';
|
|
460
|
+
// 与各方言 escapeString 一致:单引号 doubling,避免默认值中的引号破坏 SQL
|
|
441
461
|
if (typeof value === 'string')
|
|
442
|
-
return `'${value}'`;
|
|
462
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
443
463
|
if (typeof value === 'boolean')
|
|
444
464
|
return value ? '1' : '0';
|
|
445
465
|
return String(value);
|
|
@@ -400,7 +400,13 @@ export class RelatedDatabase extends Database {
|
|
|
400
400
|
}
|
|
401
401
|
else {
|
|
402
402
|
const value = condition[key];
|
|
403
|
-
|
|
403
|
+
// 只有普通对象才视为操作符集合;Date 等带原型的对象按标量等值处理,避免条件被静默丢弃
|
|
404
|
+
const isPlainObject = value !== null &&
|
|
405
|
+
typeof value === 'object' &&
|
|
406
|
+
!Array.isArray(value) &&
|
|
407
|
+
!isSubquery(value) &&
|
|
408
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
409
|
+
if (isPlainObject) {
|
|
404
410
|
for (const op in value) {
|
|
405
411
|
const quotedKey = formatField(key);
|
|
406
412
|
const placeholder = this.dialect.getParameterPlaceholder(params.length);
|
|
@@ -476,6 +482,8 @@ export class RelatedDatabase extends Database {
|
|
|
476
482
|
clauses.push(`${quotedKey} NOT LIKE ${placeholder}`);
|
|
477
483
|
params.push(value[op]);
|
|
478
484
|
break;
|
|
485
|
+
default:
|
|
486
|
+
throw new Error(`parseCondition: unknown operator "${op}" for field "${String(key)}"`);
|
|
479
487
|
}
|
|
480
488
|
}
|
|
481
489
|
}
|
|
@@ -323,7 +323,8 @@ export class RelatedModel extends Model {
|
|
|
323
323
|
const { table: pivotTable, foreignPivotKey, relatedPivotKey, pivotFields } = relation.pivot;
|
|
324
324
|
const targetKey = relation.targetKey || 'id';
|
|
325
325
|
// 查询中间表获取关联的目标ID
|
|
326
|
-
const
|
|
326
|
+
const pivotDialect = targetDb.dialect;
|
|
327
|
+
const pivotRecords = await targetDb.query(`SELECT * FROM ${pivotDialect.quoteIdentifier(pivotTable)} WHERE ${pivotDialect.quoteIdentifier(foreignPivotKey)} = ${pivotDialect.getParameterPlaceholder(0)}`, [localValue]);
|
|
327
328
|
if (pivotRecords.length === 0) {
|
|
328
329
|
return [];
|
|
329
330
|
}
|
|
@@ -420,8 +421,9 @@ export class RelatedModel extends Model {
|
|
|
420
421
|
return;
|
|
421
422
|
}
|
|
422
423
|
// 批量查询中间表
|
|
423
|
-
const
|
|
424
|
-
const
|
|
424
|
+
const batchDialect = targetDb.dialect;
|
|
425
|
+
const placeholders = localValues.map((_, i) => batchDialect.getParameterPlaceholder(i)).join(', ');
|
|
426
|
+
const pivotRecords = await targetDb.query(`SELECT * FROM ${batchDialect.quoteIdentifier(pivotTable)} WHERE ${batchDialect.quoteIdentifier(foreignPivotKey)} IN (${placeholders})`, localValues);
|
|
425
427
|
if (pivotRecords.length === 0) {
|
|
426
428
|
records.forEach(r => r[relationName] = []);
|
|
427
429
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/database",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.79",
|
|
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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"node": ">=22.5.0"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@zhin.js/logger": "1.0.
|
|
23
|
+
"@zhin.js/logger": "1.0.76"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"mongodb": ">=3.1.13",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@types/pg": "^8.
|
|
46
|
+
"@types/pg": "^8.20.0",
|
|
47
47
|
"mongodb": "^7.5.0",
|
|
48
|
-
"mysql2": "^3.
|
|
48
|
+
"mysql2": "^3.23.2",
|
|
49
49
|
"pg": "^8.22.0",
|
|
50
50
|
"redis": "latest",
|
|
51
51
|
"typescript": "^6.0.3"
|