jsql-neo 1.1.0 → 1.2.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/index.js +2 -1
- package/lib/database.js +258 -98
- package/lib/query.js +435 -0
- package/lib/table.js +350 -299
- package/package.json +1 -1
package/lib/table.js
CHANGED
|
@@ -1,36 +1,61 @@
|
|
|
1
1
|
// © Vexify 2026 All Rights Reserved.
|
|
2
2
|
/**
|
|
3
3
|
* JSQL Table — 表操作引擎
|
|
4
|
-
*
|
|
4
|
+
* 支持 DEFAULT、CHECK、外键、UPSERT、TRUNCATE、ALTER TABLE、计算字段、JSON 路径、软删除
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const Query = require('./query');
|
|
8
|
+
|
|
7
9
|
class Table {
|
|
8
10
|
constructor(name, schema, db) {
|
|
9
11
|
this._name = name;
|
|
10
12
|
this._schema = schema;
|
|
11
13
|
this._db = db;
|
|
12
|
-
this._rows = [];
|
|
13
|
-
this._indexes = {};
|
|
14
|
-
this._autoIncrement = 0;
|
|
15
|
-
this._dirty = false;
|
|
16
|
-
this._hooks = {
|
|
17
|
-
beforeInsert: [],
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
afterUpdate: [],
|
|
21
|
-
beforeRemove: [],
|
|
22
|
-
afterRemove: []
|
|
14
|
+
this._rows = [];
|
|
15
|
+
this._indexes = {};
|
|
16
|
+
this._autoIncrement = 0;
|
|
17
|
+
this._dirty = false;
|
|
18
|
+
this._hooks = {
|
|
19
|
+
beforeInsert: [], afterInsert: [],
|
|
20
|
+
beforeUpdate: [], afterUpdate: [],
|
|
21
|
+
beforeRemove: [], afterRemove: []
|
|
23
22
|
};
|
|
23
|
+
this._softDelete = false; // 是否启用软删除
|
|
24
|
+
this._computedFields = {}; // 计算字段
|
|
24
25
|
|
|
25
|
-
//
|
|
26
|
+
// 解析列定义
|
|
26
27
|
this._primaryKey = null;
|
|
27
28
|
this._autoIncrementField = null;
|
|
29
|
+
this._foreignKeys = [];
|
|
30
|
+
this._checkConstraints = [];
|
|
31
|
+
this._defaults = {};
|
|
32
|
+
|
|
28
33
|
for (const [field, def] of Object.entries(schema)) {
|
|
29
34
|
if (def.primaryKey) this._primaryKey = field;
|
|
30
35
|
if (def.autoIncrement) {
|
|
31
36
|
this._autoIncrementField = field;
|
|
32
37
|
if (def.primaryKey) this._primaryKey = field;
|
|
33
38
|
}
|
|
39
|
+
if (def.default !== undefined) {
|
|
40
|
+
this._defaults[field] = def.default;
|
|
41
|
+
}
|
|
42
|
+
if (def.check) {
|
|
43
|
+
this._checkConstraints.push({ field, fn: def.check });
|
|
44
|
+
}
|
|
45
|
+
if (def.foreignKey) {
|
|
46
|
+
this._foreignKeys.push({
|
|
47
|
+
field,
|
|
48
|
+
table: def.foreignKey.table,
|
|
49
|
+
onDelete: def.foreignKey.onDelete || 'restrict'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (def.computed) {
|
|
53
|
+
this._computedFields[field] = def.computed;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (schema._softDelete) {
|
|
58
|
+
this._softDelete = true;
|
|
34
59
|
}
|
|
35
60
|
}
|
|
36
61
|
|
|
@@ -39,19 +64,32 @@ class Table {
|
|
|
39
64
|
// ============================================================
|
|
40
65
|
|
|
41
66
|
insert(data) {
|
|
42
|
-
// 触发 beforeInsert 钩子
|
|
43
67
|
for (const hook of this._hooks.beforeInsert) {
|
|
44
68
|
data = hook({ ...data }) || data;
|
|
45
69
|
}
|
|
46
70
|
|
|
47
|
-
//
|
|
71
|
+
// 填充默认值
|
|
72
|
+
data = this._applyDefaults(data);
|
|
73
|
+
|
|
74
|
+
// 计算字段
|
|
75
|
+
data = this._applyComputed(data);
|
|
76
|
+
|
|
77
|
+
// 校验必填
|
|
48
78
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
79
|
+
if (field === '_softDelete') continue;
|
|
49
80
|
if (def.required && (data[field] === undefined || data[field] === null)) {
|
|
50
81
|
throw new Error(`Field '${field}' is required in table '${this._name}'`);
|
|
51
82
|
}
|
|
52
83
|
}
|
|
53
84
|
|
|
54
|
-
//
|
|
85
|
+
// CHECK 约束
|
|
86
|
+
for (const { field, fn } of this._checkConstraints) {
|
|
87
|
+
if (data[field] !== undefined && !fn(data[field], data)) {
|
|
88
|
+
throw new Error(`CHECK constraint failed on '${this._name}.${field}'`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 唯一约束
|
|
55
93
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
56
94
|
if (def.unique && data[field] !== undefined) {
|
|
57
95
|
for (const row of this._rows) {
|
|
@@ -63,25 +101,19 @@ class Table {
|
|
|
63
101
|
}
|
|
64
102
|
|
|
65
103
|
// 自增
|
|
66
|
-
if (this._autoIncrementField) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this._autoIncrement = data[this._autoIncrementField];
|
|
72
|
-
}
|
|
104
|
+
if (this._autoIncrementField && data[this._autoIncrementField] === undefined) {
|
|
105
|
+
this._autoIncrement++;
|
|
106
|
+
data[this._autoIncrementField] = this._autoIncrement;
|
|
107
|
+
} else if (this._autoIncrementField && data[this._autoIncrementField] > this._autoIncrement) {
|
|
108
|
+
this._autoIncrement = data[this._autoIncrementField];
|
|
73
109
|
}
|
|
74
110
|
|
|
75
111
|
const rowIndex = this._rows.length;
|
|
76
112
|
this._rows.push({ ...data });
|
|
77
|
-
|
|
78
|
-
// 更新索引
|
|
79
113
|
this._updateIndexes(rowIndex, data);
|
|
80
|
-
|
|
81
114
|
this._dirty = true;
|
|
82
115
|
this._db._markDirty();
|
|
83
116
|
|
|
84
|
-
// 触发 afterInsert 钩子
|
|
85
117
|
for (const hook of this._hooks.afterInsert) {
|
|
86
118
|
hook(data);
|
|
87
119
|
}
|
|
@@ -93,62 +125,52 @@ class Table {
|
|
|
93
125
|
return items.map(item => this.insert(item));
|
|
94
126
|
}
|
|
95
127
|
|
|
128
|
+
/**
|
|
129
|
+
* UPSERT — 有则更新,无则插入
|
|
130
|
+
* 按主键或唯一字段判断
|
|
131
|
+
*/
|
|
132
|
+
upsert(data) {
|
|
133
|
+
if (this._primaryKey && data[this._primaryKey] !== undefined) {
|
|
134
|
+
const existing = this.findById(data[this._primaryKey]);
|
|
135
|
+
if (existing) {
|
|
136
|
+
this.updateById(data[this._primaryKey], data);
|
|
137
|
+
return this.findById(data[this._primaryKey]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return this.insert(data);
|
|
141
|
+
}
|
|
142
|
+
|
|
96
143
|
// ============================================================
|
|
97
144
|
// 查询
|
|
98
145
|
// ============================================================
|
|
99
146
|
|
|
100
|
-
/**
|
|
101
|
-
* 查找所有匹配的行
|
|
102
|
-
*/
|
|
103
147
|
find(query = {}) {
|
|
104
|
-
let rows = this.
|
|
105
|
-
|
|
148
|
+
let rows = this._getVisibleRows();
|
|
106
149
|
if (Object.keys(query).length > 0) {
|
|
107
150
|
rows = this._applyFilter(rows, query);
|
|
108
151
|
}
|
|
109
|
-
|
|
110
152
|
return rows.map(r => ({ ...r }));
|
|
111
153
|
}
|
|
112
154
|
|
|
113
|
-
/**
|
|
114
|
-
* 查找第一条匹配的行
|
|
115
|
-
*/
|
|
116
155
|
findOne(query = {}) {
|
|
117
156
|
const rows = this.find(query);
|
|
118
157
|
return rows.length > 0 ? rows[0] : null;
|
|
119
158
|
}
|
|
120
159
|
|
|
121
|
-
/**
|
|
122
|
-
* 按主键查找
|
|
123
|
-
*/
|
|
124
160
|
findById(id) {
|
|
125
|
-
if (!this._primaryKey) {
|
|
126
|
-
throw new Error(`Table '${this._name}' has no primary key`);
|
|
127
|
-
}
|
|
161
|
+
if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
|
|
128
162
|
return this.findOne({ [this._primaryKey]: id });
|
|
129
163
|
}
|
|
130
164
|
|
|
131
|
-
/**
|
|
132
|
-
* 获取所有行
|
|
133
|
-
*/
|
|
134
165
|
findAll() {
|
|
135
|
-
return this.
|
|
166
|
+
return this._getVisibleRows().map(r => ({ ...r }));
|
|
136
167
|
}
|
|
137
168
|
|
|
138
|
-
/**
|
|
139
|
-
* 计数
|
|
140
|
-
*/
|
|
141
169
|
count(query = {}) {
|
|
142
|
-
if (Object.keys(query).length === 0)
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
return this._applyFilter(this._rows, query).length;
|
|
170
|
+
if (Object.keys(query).length === 0) return this._getVisibleRows().length;
|
|
171
|
+
return this._applyFilter(this._getVisibleRows(), query).length;
|
|
146
172
|
}
|
|
147
173
|
|
|
148
|
-
// ============================================================
|
|
149
|
-
// 链式查询
|
|
150
|
-
// ============================================================
|
|
151
|
-
|
|
152
174
|
where(conditions) {
|
|
153
175
|
return new Query(this, conditions);
|
|
154
176
|
}
|
|
@@ -158,12 +180,11 @@ class Table {
|
|
|
158
180
|
// ============================================================
|
|
159
181
|
|
|
160
182
|
update(query, updates) {
|
|
161
|
-
// 触发 beforeUpdate 钩子
|
|
162
183
|
for (const hook of this._hooks.beforeUpdate) {
|
|
163
184
|
updates = hook(query, { ...updates }) || updates;
|
|
164
185
|
}
|
|
165
186
|
|
|
166
|
-
const matched = this._applyFilter(this.
|
|
187
|
+
const matched = this._applyFilter(this._getVisibleRows(), query);
|
|
167
188
|
let count = 0;
|
|
168
189
|
|
|
169
190
|
for (const row of matched) {
|
|
@@ -172,7 +193,7 @@ class Table {
|
|
|
172
193
|
|
|
173
194
|
const oldData = { ...this._rows[index] };
|
|
174
195
|
|
|
175
|
-
//
|
|
196
|
+
// 唯一约束
|
|
176
197
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
177
198
|
if (def.unique && updates[field] !== undefined) {
|
|
178
199
|
for (let i = 0; i < this._rows.length; i++) {
|
|
@@ -183,21 +204,23 @@ class Table {
|
|
|
183
204
|
}
|
|
184
205
|
}
|
|
185
206
|
|
|
186
|
-
//
|
|
187
|
-
|
|
207
|
+
// CHECK 约束
|
|
208
|
+
const merged = { ...this._rows[index], ...updates };
|
|
209
|
+
for (const { field, fn } of this._checkConstraints) {
|
|
210
|
+
if (merged[field] !== undefined && !fn(merged[field], merged)) {
|
|
211
|
+
throw new Error(`CHECK constraint failed on '${this._name}.${field}'`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
188
214
|
|
|
189
|
-
|
|
215
|
+
Object.assign(this._rows[index], updates);
|
|
190
216
|
this._removeFromIndexes(index, oldData);
|
|
191
217
|
this._updateIndexes(index, this._rows[index]);
|
|
192
|
-
|
|
193
218
|
count++;
|
|
194
219
|
}
|
|
195
220
|
|
|
196
221
|
if (count > 0) {
|
|
197
222
|
this._dirty = true;
|
|
198
223
|
this._db._markDirty();
|
|
199
|
-
|
|
200
|
-
// 触发 afterUpdate 钩子
|
|
201
224
|
for (const hook of this._hooks.afterUpdate) {
|
|
202
225
|
hook(query, updates, count);
|
|
203
226
|
}
|
|
@@ -206,9 +229,7 @@ class Table {
|
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
updateById(id, updates) {
|
|
209
|
-
if (!this._primaryKey) {
|
|
210
|
-
throw new Error(`Table '${this._name}' has no primary key`);
|
|
211
|
-
}
|
|
232
|
+
if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
|
|
212
233
|
return this.update({ [this._primaryKey]: id }, updates);
|
|
213
234
|
}
|
|
214
235
|
|
|
@@ -217,65 +238,183 @@ class Table {
|
|
|
217
238
|
// ============================================================
|
|
218
239
|
|
|
219
240
|
remove(query = {}) {
|
|
220
|
-
// 触发 beforeRemove 钩子
|
|
221
241
|
for (const hook of this._hooks.beforeRemove) {
|
|
222
|
-
|
|
223
|
-
if (result === false) return 0; // 钩子取消删除
|
|
242
|
+
if (hook(query) === false) return 0;
|
|
224
243
|
}
|
|
225
244
|
|
|
226
245
|
if (Object.keys(query).length === 0) {
|
|
227
|
-
|
|
228
|
-
const count = this._rows.length;
|
|
229
|
-
this._rows = [];
|
|
230
|
-
this._indexes = {};
|
|
231
|
-
this._autoIncrement = 0;
|
|
232
|
-
this._dirty = true;
|
|
233
|
-
this._db._markDirty();
|
|
234
|
-
return count;
|
|
246
|
+
return this._truncate();
|
|
235
247
|
}
|
|
236
248
|
|
|
237
|
-
const toRemove = this._applyFilter(this.
|
|
249
|
+
const toRemove = this._applyFilter(this._getVisibleRows(), query);
|
|
250
|
+
let count = 0;
|
|
251
|
+
|
|
238
252
|
for (const row of toRemove) {
|
|
239
253
|
const index = this._rows.indexOf(row);
|
|
240
|
-
if (index
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
.filter(p => p !== index)
|
|
249
|
-
.map(p => p > index ? p - 1 : p);
|
|
250
|
-
if (newPositions.length === 0) {
|
|
251
|
-
idx.delete(key);
|
|
252
|
-
} else {
|
|
253
|
-
idx.set(key, newPositions);
|
|
254
|
-
}
|
|
254
|
+
if (index === -1) continue;
|
|
255
|
+
|
|
256
|
+
// 外键级联删除
|
|
257
|
+
for (const fk of this._foreignKeys) {
|
|
258
|
+
if (fk.onDelete === 'cascade') {
|
|
259
|
+
const refTable = this._db._tables[fk.table];
|
|
260
|
+
if (refTable) {
|
|
261
|
+
refTable.remove({ [fk.field]: row[this._primaryKey] });
|
|
255
262
|
}
|
|
256
263
|
}
|
|
257
264
|
}
|
|
265
|
+
|
|
266
|
+
if (this._softDelete) {
|
|
267
|
+
this._rows[index]._deleted = true;
|
|
268
|
+
this._rows[index]._deletedAt = Date.now();
|
|
269
|
+
} else {
|
|
270
|
+
this._removeFromIndexes(index, row);
|
|
271
|
+
this._rows.splice(index, 1);
|
|
272
|
+
this._adjustIndexesAfterRemove(index);
|
|
273
|
+
}
|
|
274
|
+
count++;
|
|
258
275
|
}
|
|
259
276
|
|
|
260
|
-
if (
|
|
277
|
+
if (count > 0) {
|
|
261
278
|
this._dirty = true;
|
|
262
279
|
this._db._markDirty();
|
|
263
|
-
|
|
264
|
-
// 触发 afterRemove 钩子
|
|
265
280
|
for (const hook of this._hooks.afterRemove) {
|
|
266
|
-
hook(query,
|
|
281
|
+
hook(query, count);
|
|
267
282
|
}
|
|
268
283
|
}
|
|
269
|
-
return
|
|
284
|
+
return count;
|
|
270
285
|
}
|
|
271
286
|
|
|
272
287
|
removeById(id) {
|
|
273
|
-
if (!this._primaryKey) {
|
|
274
|
-
throw new Error(`Table '${this._name}' has no primary key`);
|
|
275
|
-
}
|
|
288
|
+
if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
|
|
276
289
|
return this.remove({ [this._primaryKey]: id });
|
|
277
290
|
}
|
|
278
291
|
|
|
292
|
+
/**
|
|
293
|
+
* 软删除 — 标记为已删除,可恢复
|
|
294
|
+
*/
|
|
295
|
+
removeSoft(query = {}) {
|
|
296
|
+
this._softDelete = true;
|
|
297
|
+
const result = this.remove(query);
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 恢复软删除的行
|
|
303
|
+
*/
|
|
304
|
+
restore(query = {}) {
|
|
305
|
+
if (!this._softDelete) throw new Error('Soft delete is not enabled');
|
|
306
|
+
const matched = this._applyFilter(this._rows, { ...query, _deleted: true });
|
|
307
|
+
for (const row of matched) {
|
|
308
|
+
const index = this._rows.indexOf(row);
|
|
309
|
+
if (index !== -1) {
|
|
310
|
+
delete this._rows[index]._deleted;
|
|
311
|
+
delete this._rows[index]._deletedAt;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return matched.length;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 清空表(保留结构)
|
|
319
|
+
*/
|
|
320
|
+
truncate() {
|
|
321
|
+
return this._truncate();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
_truncate() {
|
|
325
|
+
const count = this._rows.length;
|
|
326
|
+
this._rows = [];
|
|
327
|
+
this._indexes = {};
|
|
328
|
+
this._autoIncrement = 0;
|
|
329
|
+
this._dirty = true;
|
|
330
|
+
this._db._markDirty();
|
|
331
|
+
return count;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================================
|
|
335
|
+
// ALTER TABLE
|
|
336
|
+
// ============================================================
|
|
337
|
+
|
|
338
|
+
addColumn(field, definition) {
|
|
339
|
+
if (this._schema[field]) throw new Error(`Column '${field}' already exists`);
|
|
340
|
+
this._schema[field] = definition;
|
|
341
|
+
if (definition.default !== undefined) this._defaults[field] = definition.default;
|
|
342
|
+
if (definition.check) this._checkConstraints.push({ field, fn: definition.check });
|
|
343
|
+
if (definition.foreignKey) {
|
|
344
|
+
this._foreignKeys.push({
|
|
345
|
+
field,
|
|
346
|
+
table: definition.foreignKey.table,
|
|
347
|
+
onDelete: definition.foreignKey.onDelete || 'restrict'
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (definition.computed) this._computedFields[field] = definition.computed;
|
|
351
|
+
this._dirty = true;
|
|
352
|
+
this._db._markDirty();
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
dropColumn(field) {
|
|
357
|
+
if (!this._schema[field]) throw new Error(`Column '${field}' does not exist`);
|
|
358
|
+
delete this._schema[field];
|
|
359
|
+
delete this._defaults[field];
|
|
360
|
+
this._checkConstraints = this._checkConstraints.filter(c => c.field !== field);
|
|
361
|
+
this._foreignKeys = this._foreignKeys.filter(fk => fk.field !== field);
|
|
362
|
+
delete this._computedFields[field];
|
|
363
|
+
// 移除所有行中的该字段
|
|
364
|
+
for (const row of this._rows) {
|
|
365
|
+
delete row[field];
|
|
366
|
+
}
|
|
367
|
+
this._dirty = true;
|
|
368
|
+
this._db._markDirty();
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
renameColumn(oldName, newName) {
|
|
373
|
+
if (!this._schema[oldName]) throw new Error(`Column '${oldName}' does not exist`);
|
|
374
|
+
if (this._schema[newName]) throw new Error(`Column '${newName}' already exists`);
|
|
375
|
+
this._schema[newName] = this._schema[oldName];
|
|
376
|
+
delete this._schema[oldName];
|
|
377
|
+
if (this._defaults[oldName] !== undefined) {
|
|
378
|
+
this._defaults[newName] = this._defaults[oldName];
|
|
379
|
+
delete this._defaults[oldName];
|
|
380
|
+
}
|
|
381
|
+
for (const row of this._rows) {
|
|
382
|
+
row[newName] = row[oldName];
|
|
383
|
+
delete row[oldName];
|
|
384
|
+
}
|
|
385
|
+
if (this._primaryKey === oldName) this._primaryKey = newName;
|
|
386
|
+
if (this._autoIncrementField === oldName) this._autoIncrementField = newName;
|
|
387
|
+
this._dirty = true;
|
|
388
|
+
this._db._markDirty();
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
modifyColumn(field, definition) {
|
|
393
|
+
if (!this._schema[field]) throw new Error(`Column '${field}' does not exist`);
|
|
394
|
+
this._schema[field] = { ...this._schema[field], ...definition };
|
|
395
|
+
if (definition.default !== undefined) this._defaults[field] = definition.default;
|
|
396
|
+
this._checkConstraints = this._checkConstraints.filter(c => c.field !== field);
|
|
397
|
+
if (definition.check) this._checkConstraints.push({ field, fn: definition.check });
|
|
398
|
+
this._dirty = true;
|
|
399
|
+
this._db._markDirty();
|
|
400
|
+
return this;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 获取表结构
|
|
405
|
+
*/
|
|
406
|
+
describe() {
|
|
407
|
+
return {
|
|
408
|
+
name: this._name,
|
|
409
|
+
columns: { ...this._schema },
|
|
410
|
+
primaryKey: this._primaryKey,
|
|
411
|
+
rowCount: this._getVisibleRows().length,
|
|
412
|
+
indexCount: Object.keys(this._indexes).length,
|
|
413
|
+
softDelete: this._softDelete,
|
|
414
|
+
foreignKeys: this._foreignKeys.map(fk => ({ field: fk.field, table: fk.table }))
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
279
418
|
// ============================================================
|
|
280
419
|
// 索引
|
|
281
420
|
// ============================================================
|
|
@@ -302,17 +441,6 @@ class Table {
|
|
|
302
441
|
// 事件钩子
|
|
303
442
|
// ============================================================
|
|
304
443
|
|
|
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
444
|
on(event, callback) {
|
|
317
445
|
if (this._hooks[event]) {
|
|
318
446
|
this._hooks[event].push(callback);
|
|
@@ -328,7 +456,7 @@ class Table {
|
|
|
328
456
|
}
|
|
329
457
|
|
|
330
458
|
// ============================================================
|
|
331
|
-
//
|
|
459
|
+
// 内部: 过滤器
|
|
332
460
|
// ============================================================
|
|
333
461
|
|
|
334
462
|
_applyFilter(rows, query) {
|
|
@@ -343,20 +471,29 @@ class Table {
|
|
|
343
471
|
|
|
344
472
|
_matchRow(row, query) {
|
|
345
473
|
for (const [field, condition] of Object.entries(query)) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (condition !== null && typeof condition === 'object' && !Array.isArray(condition)) {
|
|
350
|
-
|
|
351
|
-
|
|
474
|
+
// JSON 路径: 'profile.city' 或 'data.profile.city'
|
|
475
|
+
const value = this._resolvePath(row, field);
|
|
476
|
+
|
|
477
|
+
if (condition !== null && typeof condition === 'object' && !Array.isArray(condition) && !(condition instanceof RegExp)) {
|
|
478
|
+
// 检查是否是操作符
|
|
479
|
+
const keys = Object.keys(condition);
|
|
480
|
+
const isOperator = keys.every(k => k.startsWith('$'));
|
|
481
|
+
if (isOperator && keys.length > 0) {
|
|
482
|
+
for (const [op, target] of Object.entries(condition)) {
|
|
483
|
+
if (!this._matchOperator(value, op, target)) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
// 普通对象:递归匹配子文档
|
|
489
|
+
if (value !== null && typeof value === 'object') {
|
|
490
|
+
if (!this._matchRow(value, condition)) return false;
|
|
491
|
+
} else {
|
|
352
492
|
return false;
|
|
353
493
|
}
|
|
354
494
|
}
|
|
355
495
|
} else {
|
|
356
|
-
|
|
357
|
-
if (value !== condition) {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
496
|
+
if (value !== condition) return false;
|
|
360
497
|
}
|
|
361
498
|
}
|
|
362
499
|
return true;
|
|
@@ -374,8 +511,7 @@ class Table {
|
|
|
374
511
|
case '$nin': return Array.isArray(target) && !target.includes(value);
|
|
375
512
|
case '$like':
|
|
376
513
|
if (typeof value !== 'string' || typeof target !== 'string') return false;
|
|
377
|
-
|
|
378
|
-
return regex.test(value);
|
|
514
|
+
return new RegExp(target.replace(/%/g, '.*').replace(/_/g, '.'), 'i').test(value);
|
|
379
515
|
case '$regex':
|
|
380
516
|
return new RegExp(target).test(String(value));
|
|
381
517
|
case '$exists':
|
|
@@ -383,13 +519,82 @@ class Table {
|
|
|
383
519
|
case '$between':
|
|
384
520
|
return Array.isArray(target) && target.length === 2
|
|
385
521
|
&& value >= target[0] && value <= target[1];
|
|
522
|
+
case '$and':
|
|
523
|
+
return Array.isArray(target) && target.every(sub => this._matchRow({ _val: value }, { _val: sub }));
|
|
386
524
|
case '$or':
|
|
387
|
-
return Array.isArray(target) && target.some(sub => this._matchRow({
|
|
525
|
+
return Array.isArray(target) && target.some(sub => this._matchRow({ _val: value }, { _val: sub }));
|
|
526
|
+
case '$not':
|
|
527
|
+
return !this._matchRow({ _val: value }, { _val: target });
|
|
528
|
+
case '$contains':
|
|
529
|
+
return Array.isArray(value) && value.includes(target);
|
|
530
|
+
case '$type':
|
|
531
|
+
return typeof value === target;
|
|
532
|
+
case '$size':
|
|
533
|
+
return Array.isArray(value) && value.length === target;
|
|
534
|
+
case '$elemMatch':
|
|
535
|
+
return Array.isArray(value) && value.some(el =>
|
|
536
|
+
typeof el === 'object' ? this._matchRow(el, target) : el === target
|
|
537
|
+
);
|
|
538
|
+
case '$isNull':
|
|
539
|
+
return target ? value === null || value === undefined : value !== null && value !== undefined;
|
|
540
|
+
case '$startsWith':
|
|
541
|
+
return typeof value === 'string' && value.startsWith(target);
|
|
542
|
+
case '$endsWith':
|
|
543
|
+
return typeof value === 'string' && value.endsWith(target);
|
|
544
|
+
case '$search':
|
|
545
|
+
// 全文搜索(简单实现)
|
|
546
|
+
if (typeof value !== 'string') return false;
|
|
547
|
+
const terms = Array.isArray(target) ? target : [target];
|
|
548
|
+
return terms.every(t => value.toLowerCase().includes(t.toLowerCase()));
|
|
388
549
|
default:
|
|
389
550
|
return false;
|
|
390
551
|
}
|
|
391
552
|
}
|
|
392
553
|
|
|
554
|
+
/**
|
|
555
|
+
* 解析 JSON 路径,如 'profile.city' -> row.profile.city
|
|
556
|
+
*/
|
|
557
|
+
_resolvePath(row, field) {
|
|
558
|
+
if (field.includes('.') && !field.startsWith('$')) {
|
|
559
|
+
const parts = field.split('.');
|
|
560
|
+
let value = row;
|
|
561
|
+
for (const part of parts) {
|
|
562
|
+
if (value === null || value === undefined) return undefined;
|
|
563
|
+
value = value[part];
|
|
564
|
+
}
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
return row[field];
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ============================================================
|
|
571
|
+
// 内部: 辅助
|
|
572
|
+
// ============================================================
|
|
573
|
+
|
|
574
|
+
_applyDefaults(data) {
|
|
575
|
+
const result = { ...data };
|
|
576
|
+
for (const [field, defaultValue] of Object.entries(this._defaults)) {
|
|
577
|
+
if (result[field] === undefined) {
|
|
578
|
+
result[field] = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
_applyComputed(data) {
|
|
585
|
+
for (const [field, fn] of Object.entries(this._computedFields)) {
|
|
586
|
+
data[field] = fn(data);
|
|
587
|
+
}
|
|
588
|
+
return data;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
_getVisibleRows() {
|
|
592
|
+
if (this._softDelete) {
|
|
593
|
+
return this._rows.filter(r => !r._deleted);
|
|
594
|
+
}
|
|
595
|
+
return this._rows;
|
|
596
|
+
}
|
|
597
|
+
|
|
393
598
|
_updateIndexes(rowIndex, data) {
|
|
394
599
|
for (const field of Object.keys(this._indexes)) {
|
|
395
600
|
const value = data[field];
|
|
@@ -416,9 +621,24 @@ class Table {
|
|
|
416
621
|
}
|
|
417
622
|
}
|
|
418
623
|
|
|
624
|
+
_adjustIndexesAfterRemove(removedIndex) {
|
|
625
|
+
for (const idxName of Object.keys(this._indexes)) {
|
|
626
|
+
const idx = this._indexes[idxName];
|
|
627
|
+
for (const [key, positions] of idx) {
|
|
628
|
+
const newPositions = positions
|
|
629
|
+
.filter(p => p !== removedIndex)
|
|
630
|
+
.map(p => p > removedIndex ? p - 1 : p);
|
|
631
|
+
if (newPositions.length === 0) {
|
|
632
|
+
idx.delete(key);
|
|
633
|
+
} else {
|
|
634
|
+
idx.set(key, newPositions);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
419
640
|
_loadRows(rows) {
|
|
420
641
|
this._rows = rows;
|
|
421
|
-
// 重建自增
|
|
422
642
|
if (this._autoIncrementField) {
|
|
423
643
|
for (const row of rows) {
|
|
424
644
|
const val = row[this._autoIncrementField];
|
|
@@ -427,183 +647,14 @@ class Table {
|
|
|
427
647
|
}
|
|
428
648
|
}
|
|
429
649
|
}
|
|
430
|
-
// 重建所有索引
|
|
431
650
|
for (const field of Object.keys(this._indexes)) {
|
|
432
651
|
this.createIndex(field);
|
|
433
652
|
}
|
|
434
653
|
}
|
|
435
654
|
|
|
436
|
-
// ============================================================
|
|
437
|
-
// 导出
|
|
438
|
-
// ============================================================
|
|
439
|
-
|
|
440
655
|
toJSON() {
|
|
441
656
|
return this._rows;
|
|
442
657
|
}
|
|
443
658
|
}
|
|
444
659
|
|
|
445
|
-
// ============================================================
|
|
446
|
-
// 链式查询构建器
|
|
447
|
-
// ============================================================
|
|
448
|
-
|
|
449
|
-
class Query {
|
|
450
|
-
constructor(table, conditions) {
|
|
451
|
-
this._table = table;
|
|
452
|
-
this._conditions = conditions;
|
|
453
|
-
this._orderBy = null;
|
|
454
|
-
this._orderDir = 'asc';
|
|
455
|
-
this._limitCount = null;
|
|
456
|
-
this._offsetCount = 0;
|
|
457
|
-
this._selectFields = null;
|
|
458
|
-
this._groupBy = null;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
orderBy(field, dir = 'asc') {
|
|
462
|
-
this._orderBy = field;
|
|
463
|
-
this._orderDir = dir;
|
|
464
|
-
return this;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
limit(n) {
|
|
468
|
-
this._limitCount = n;
|
|
469
|
-
return this;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
offset(n) {
|
|
473
|
-
this._offsetCount = n;
|
|
474
|
-
return this;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
select(fields) {
|
|
478
|
-
this._selectFields = Array.isArray(fields) ? fields : [fields];
|
|
479
|
-
return this;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
groupBy(field) {
|
|
483
|
-
this._groupBy = field;
|
|
484
|
-
return this;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
get() {
|
|
488
|
-
let rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
489
|
-
|
|
490
|
-
// 排序
|
|
491
|
-
if (this._orderBy) {
|
|
492
|
-
rows = rows.sort((a, b) => {
|
|
493
|
-
const va = a[this._orderBy];
|
|
494
|
-
const vb = b[this._orderBy];
|
|
495
|
-
if (va < vb) return this._orderDir === 'asc' ? -1 : 1;
|
|
496
|
-
if (va > vb) return this._orderDir === 'asc' ? 1 : -1;
|
|
497
|
-
return 0;
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// 分页
|
|
502
|
-
if (this._offsetCount > 0) {
|
|
503
|
-
rows = rows.slice(this._offsetCount);
|
|
504
|
-
}
|
|
505
|
-
if (this._limitCount !== null) {
|
|
506
|
-
rows = rows.slice(0, this._limitCount);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// 字段选择
|
|
510
|
-
rows = rows.map(r => ({ ...r }));
|
|
511
|
-
if (this._selectFields) {
|
|
512
|
-
rows = rows.map(r => {
|
|
513
|
-
const obj = {};
|
|
514
|
-
for (const f of this._selectFields) {
|
|
515
|
-
obj[f] = r[f];
|
|
516
|
-
}
|
|
517
|
-
return obj;
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return rows;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
first() {
|
|
525
|
-
const rows = this.limit(1).get();
|
|
526
|
-
return rows.length > 0 ? rows[0] : null;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
count() {
|
|
530
|
-
const rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
531
|
-
return rows.length;
|
|
532
|
-
}
|
|
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
|
-
|
|
600
|
-
update(updates) {
|
|
601
|
-
return this._table.update(this._conditions, updates);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
remove() {
|
|
605
|
-
return this._table.remove(this._conditions);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
660
|
module.exports = Table;
|