jsql-neo 1.0.0 → 1.1.0
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/database.js +98 -0
- package/lib/table.js +141 -0
- package/package.json +1 -1
package/lib/database.js
CHANGED
|
@@ -28,6 +28,7 @@ class Database {
|
|
|
28
28
|
this._dirty = false;
|
|
29
29
|
this._autoSaveTimer = null;
|
|
30
30
|
this._loading = false; // 加载中,不触发自动保存
|
|
31
|
+
this._transaction = null; // 事务快照 { tables, autoIncrements }
|
|
31
32
|
|
|
32
33
|
// 从文件加载
|
|
33
34
|
if (!this._memoryMode && fs.existsSync(this._filePath)) {
|
|
@@ -96,6 +97,103 @@ class Database {
|
|
|
96
97
|
return Object.keys(this._tables);
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
// ============================================================
|
|
101
|
+
// 事务
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 开始事务
|
|
106
|
+
* 保存当前数据快照,后续操作可回滚
|
|
107
|
+
*/
|
|
108
|
+
begin() {
|
|
109
|
+
if (this._transaction) {
|
|
110
|
+
throw new Error('Transaction already in progress');
|
|
111
|
+
}
|
|
112
|
+
this._transaction = {
|
|
113
|
+
rows: {},
|
|
114
|
+
autoIncrements: {}
|
|
115
|
+
};
|
|
116
|
+
for (const [name, table] of Object.entries(this._tables)) {
|
|
117
|
+
this._transaction.rows[name] = table._rows.map(r => ({ ...r }));
|
|
118
|
+
this._transaction.autoIncrements[name] = table._autoIncrement;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 提交事务
|
|
124
|
+
*/
|
|
125
|
+
commit() {
|
|
126
|
+
if (!this._transaction) {
|
|
127
|
+
throw new Error('No transaction in progress');
|
|
128
|
+
}
|
|
129
|
+
this._transaction = null;
|
|
130
|
+
this._markDirty();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 回滚事务
|
|
135
|
+
*/
|
|
136
|
+
rollback() {
|
|
137
|
+
if (!this._transaction) {
|
|
138
|
+
throw new Error('No transaction in progress');
|
|
139
|
+
}
|
|
140
|
+
for (const [name, table] of Object.entries(this._tables)) {
|
|
141
|
+
if (this._transaction.rows[name]) {
|
|
142
|
+
table._rows = this._transaction.rows[name];
|
|
143
|
+
table._autoIncrement = this._transaction.autoIncrements[name];
|
|
144
|
+
// 重建索引
|
|
145
|
+
for (const field of Object.keys(table._indexes)) {
|
|
146
|
+
table.createIndex(field);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
this._transaction = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 是否在事务中
|
|
155
|
+
*/
|
|
156
|
+
inTransaction() {
|
|
157
|
+
return !!this._transaction;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================
|
|
161
|
+
// 备份与恢复
|
|
162
|
+
// ============================================================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 备份到文件
|
|
166
|
+
*/
|
|
167
|
+
backup(filePath) {
|
|
168
|
+
const data = this.export();
|
|
169
|
+
const json = this._options.pretty
|
|
170
|
+
? JSON.stringify(data, null, 2)
|
|
171
|
+
: JSON.stringify(data);
|
|
172
|
+
const dir = path.dirname(filePath);
|
|
173
|
+
if (!fs.existsSync(dir)) {
|
|
174
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
fs.writeFileSync(filePath, json, 'utf8');
|
|
177
|
+
return filePath;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 从文件恢复(覆盖当前数据)
|
|
182
|
+
*/
|
|
183
|
+
restore(filePath) {
|
|
184
|
+
if (!fs.existsSync(filePath)) {
|
|
185
|
+
throw new Error(`Backup file not found: ${filePath}`);
|
|
186
|
+
}
|
|
187
|
+
const json = fs.readFileSync(filePath, 'utf8');
|
|
188
|
+
const data = JSON.parse(json);
|
|
189
|
+
for (const [name, rows] of Object.entries(data)) {
|
|
190
|
+
if (this._tables[name]) {
|
|
191
|
+
this._tables[name]._loadRows(rows);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this._markDirty();
|
|
195
|
+
}
|
|
196
|
+
|
|
99
197
|
// ============================================================
|
|
100
198
|
// 持久化
|
|
101
199
|
// ============================================================
|
package/lib/table.js
CHANGED
|
@@ -13,6 +13,14 @@ class Table {
|
|
|
13
13
|
this._indexes = {}; // 字段名 -> Map<value, [rowIndex, ...]>
|
|
14
14
|
this._autoIncrement = 0; // 自增计数器
|
|
15
15
|
this._dirty = false; // 是否有未保存的修改
|
|
16
|
+
this._hooks = { // 事件钩子
|
|
17
|
+
beforeInsert: [],
|
|
18
|
+
afterInsert: [],
|
|
19
|
+
beforeUpdate: [],
|
|
20
|
+
afterUpdate: [],
|
|
21
|
+
beforeRemove: [],
|
|
22
|
+
afterRemove: []
|
|
23
|
+
};
|
|
16
24
|
|
|
17
25
|
// 解析主键和自增字段
|
|
18
26
|
this._primaryKey = null;
|
|
@@ -31,6 +39,11 @@ class Table {
|
|
|
31
39
|
// ============================================================
|
|
32
40
|
|
|
33
41
|
insert(data) {
|
|
42
|
+
// 触发 beforeInsert 钩子
|
|
43
|
+
for (const hook of this._hooks.beforeInsert) {
|
|
44
|
+
data = hook({ ...data }) || data;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
// 校验必填字段
|
|
35
48
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
36
49
|
if (def.required && (data[field] === undefined || data[field] === null)) {
|
|
@@ -67,6 +80,12 @@ class Table {
|
|
|
67
80
|
|
|
68
81
|
this._dirty = true;
|
|
69
82
|
this._db._markDirty();
|
|
83
|
+
|
|
84
|
+
// 触发 afterInsert 钩子
|
|
85
|
+
for (const hook of this._hooks.afterInsert) {
|
|
86
|
+
hook(data);
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
return data;
|
|
71
90
|
}
|
|
72
91
|
|
|
@@ -139,6 +158,11 @@ class Table {
|
|
|
139
158
|
// ============================================================
|
|
140
159
|
|
|
141
160
|
update(query, updates) {
|
|
161
|
+
// 触发 beforeUpdate 钩子
|
|
162
|
+
for (const hook of this._hooks.beforeUpdate) {
|
|
163
|
+
updates = hook(query, { ...updates }) || updates;
|
|
164
|
+
}
|
|
165
|
+
|
|
142
166
|
const matched = this._applyFilter(this._rows, query);
|
|
143
167
|
let count = 0;
|
|
144
168
|
|
|
@@ -172,6 +196,11 @@ class Table {
|
|
|
172
196
|
if (count > 0) {
|
|
173
197
|
this._dirty = true;
|
|
174
198
|
this._db._markDirty();
|
|
199
|
+
|
|
200
|
+
// 触发 afterUpdate 钩子
|
|
201
|
+
for (const hook of this._hooks.afterUpdate) {
|
|
202
|
+
hook(query, updates, count);
|
|
203
|
+
}
|
|
175
204
|
}
|
|
176
205
|
return count;
|
|
177
206
|
}
|
|
@@ -188,6 +217,12 @@ class Table {
|
|
|
188
217
|
// ============================================================
|
|
189
218
|
|
|
190
219
|
remove(query = {}) {
|
|
220
|
+
// 触发 beforeRemove 钩子
|
|
221
|
+
for (const hook of this._hooks.beforeRemove) {
|
|
222
|
+
const result = hook(query);
|
|
223
|
+
if (result === false) return 0; // 钩子取消删除
|
|
224
|
+
}
|
|
225
|
+
|
|
191
226
|
if (Object.keys(query).length === 0) {
|
|
192
227
|
// 删除全部
|
|
193
228
|
const count = this._rows.length;
|
|
@@ -225,6 +260,11 @@ class Table {
|
|
|
225
260
|
if (toRemove.length > 0) {
|
|
226
261
|
this._dirty = true;
|
|
227
262
|
this._db._markDirty();
|
|
263
|
+
|
|
264
|
+
// 触发 afterRemove 钩子
|
|
265
|
+
for (const hook of this._hooks.afterRemove) {
|
|
266
|
+
hook(query, toRemove.length);
|
|
267
|
+
}
|
|
228
268
|
}
|
|
229
269
|
return toRemove.length;
|
|
230
270
|
}
|
|
@@ -258,6 +298,35 @@ class Table {
|
|
|
258
298
|
delete this._indexes[field];
|
|
259
299
|
}
|
|
260
300
|
|
|
301
|
+
// ============================================================
|
|
302
|
+
// 事件钩子
|
|
303
|
+
// ============================================================
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 注册事件钩子
|
|
307
|
+
* @param {string} event - 'beforeInsert' | 'afterInsert' | 'beforeUpdate' | 'afterUpdate' | 'beforeRemove' | 'afterRemove'
|
|
308
|
+
* @param {function} callback
|
|
309
|
+
* - beforeInsert(data) => 返回修改后的 data
|
|
310
|
+
* - afterInsert(data)
|
|
311
|
+
* - beforeUpdate(query, updates) => 返回修改后的 updates
|
|
312
|
+
* - afterUpdate(query, updates, count)
|
|
313
|
+
* - beforeRemove(query) => 返回 false 阻止删除
|
|
314
|
+
* - afterRemove(query, count)
|
|
315
|
+
*/
|
|
316
|
+
on(event, callback) {
|
|
317
|
+
if (this._hooks[event]) {
|
|
318
|
+
this._hooks[event].push(callback);
|
|
319
|
+
}
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
off(event, callback) {
|
|
324
|
+
if (this._hooks[event]) {
|
|
325
|
+
this._hooks[event] = this._hooks[event].filter(cb => cb !== callback);
|
|
326
|
+
}
|
|
327
|
+
return this;
|
|
328
|
+
}
|
|
329
|
+
|
|
261
330
|
// ============================================================
|
|
262
331
|
// 内部方法
|
|
263
332
|
// ============================================================
|
|
@@ -386,6 +455,7 @@ class Query {
|
|
|
386
455
|
this._limitCount = null;
|
|
387
456
|
this._offsetCount = 0;
|
|
388
457
|
this._selectFields = null;
|
|
458
|
+
this._groupBy = null;
|
|
389
459
|
}
|
|
390
460
|
|
|
391
461
|
orderBy(field, dir = 'asc') {
|
|
@@ -409,6 +479,11 @@ class Query {
|
|
|
409
479
|
return this;
|
|
410
480
|
}
|
|
411
481
|
|
|
482
|
+
groupBy(field) {
|
|
483
|
+
this._groupBy = field;
|
|
484
|
+
return this;
|
|
485
|
+
}
|
|
486
|
+
|
|
412
487
|
get() {
|
|
413
488
|
let rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
414
489
|
|
|
@@ -456,6 +531,72 @@ class Query {
|
|
|
456
531
|
return rows.length;
|
|
457
532
|
}
|
|
458
533
|
|
|
534
|
+
sum(field) {
|
|
535
|
+
const rows = this._applyGroupedRows();
|
|
536
|
+
return rows.reduce((acc, r) => acc + (Number(r[field]) || 0), 0);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
avg(field) {
|
|
540
|
+
const rows = this._applyGroupedRows();
|
|
541
|
+
if (rows.length === 0) return 0;
|
|
542
|
+
return this.sum(field) / rows.length;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
min(field) {
|
|
546
|
+
const rows = this._applyGroupedRows();
|
|
547
|
+
if (rows.length === 0) return null;
|
|
548
|
+
return rows.reduce((min, r) => {
|
|
549
|
+
const v = Number(r[field]);
|
|
550
|
+
return (min === null || v < min) ? v : min;
|
|
551
|
+
}, null);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
max(field) {
|
|
555
|
+
const rows = this._applyGroupedRows();
|
|
556
|
+
if (rows.length === 0) return null;
|
|
557
|
+
return rows.reduce((max, r) => {
|
|
558
|
+
const v = Number(r[field]);
|
|
559
|
+
return (max === null || v > max) ? v : max;
|
|
560
|
+
}, null);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* 获取分组统计结果
|
|
565
|
+
* @returns {Array<{ _group: value, _count, _sum, _avg, _min, _max }>}
|
|
566
|
+
*/
|
|
567
|
+
groupStats(field) {
|
|
568
|
+
if (!this._groupBy) throw new Error('groupStats() requires groupBy() to be called first');
|
|
569
|
+
const rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
570
|
+
const groups = {};
|
|
571
|
+
|
|
572
|
+
for (const row of rows) {
|
|
573
|
+
const key = String(row[this._groupBy] ?? '__null__');
|
|
574
|
+
if (!groups[key]) {
|
|
575
|
+
groups[key] = { _group: row[this._groupBy], _count: 0, _sum: 0, _min: null, _max: null, _rows: [] };
|
|
576
|
+
}
|
|
577
|
+
const g = groups[key];
|
|
578
|
+
g._count++;
|
|
579
|
+
const v = Number(row[field]) || 0;
|
|
580
|
+
g._sum += v;
|
|
581
|
+
g._min = g._min === null ? v : Math.min(g._min, v);
|
|
582
|
+
g._max = g._max === null ? v : Math.max(g._max, v);
|
|
583
|
+
g._rows.push(row);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const result = Object.values(groups);
|
|
587
|
+
result.forEach(g => {
|
|
588
|
+
g._avg = g._count > 0 ? g._sum / g._count : 0;
|
|
589
|
+
delete g._rows;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
_applyGroupedRows() {
|
|
596
|
+
let rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
597
|
+
return rows;
|
|
598
|
+
}
|
|
599
|
+
|
|
459
600
|
update(updates) {
|
|
460
601
|
return this._table.update(this._conditions, updates);
|
|
461
602
|
}
|