jsql-neo 1.1.0 → 1.2.1
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 +376 -298
- 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,29 +101,25 @@ 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
|
}
|
|
88
120
|
|
|
121
|
+
this._db._emitChange('insert', this._name, data);
|
|
122
|
+
|
|
89
123
|
return data;
|
|
90
124
|
}
|
|
91
125
|
|
|
@@ -93,62 +127,52 @@ class Table {
|
|
|
93
127
|
return items.map(item => this.insert(item));
|
|
94
128
|
}
|
|
95
129
|
|
|
130
|
+
/**
|
|
131
|
+
* UPSERT — 有则更新,无则插入
|
|
132
|
+
* 按主键或唯一字段判断
|
|
133
|
+
*/
|
|
134
|
+
upsert(data) {
|
|
135
|
+
if (this._primaryKey && data[this._primaryKey] !== undefined) {
|
|
136
|
+
const existing = this.findById(data[this._primaryKey]);
|
|
137
|
+
if (existing) {
|
|
138
|
+
this.updateById(data[this._primaryKey], data);
|
|
139
|
+
return this.findById(data[this._primaryKey]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return this.insert(data);
|
|
143
|
+
}
|
|
144
|
+
|
|
96
145
|
// ============================================================
|
|
97
146
|
// 查询
|
|
98
147
|
// ============================================================
|
|
99
148
|
|
|
100
|
-
/**
|
|
101
|
-
* 查找所有匹配的行
|
|
102
|
-
*/
|
|
103
149
|
find(query = {}) {
|
|
104
|
-
let rows = this.
|
|
105
|
-
|
|
150
|
+
let rows = this._getVisibleRows();
|
|
106
151
|
if (Object.keys(query).length > 0) {
|
|
107
152
|
rows = this._applyFilter(rows, query);
|
|
108
153
|
}
|
|
109
|
-
|
|
110
154
|
return rows.map(r => ({ ...r }));
|
|
111
155
|
}
|
|
112
156
|
|
|
113
|
-
/**
|
|
114
|
-
* 查找第一条匹配的行
|
|
115
|
-
*/
|
|
116
157
|
findOne(query = {}) {
|
|
117
158
|
const rows = this.find(query);
|
|
118
159
|
return rows.length > 0 ? rows[0] : null;
|
|
119
160
|
}
|
|
120
161
|
|
|
121
|
-
/**
|
|
122
|
-
* 按主键查找
|
|
123
|
-
*/
|
|
124
162
|
findById(id) {
|
|
125
|
-
if (!this._primaryKey) {
|
|
126
|
-
throw new Error(`Table '${this._name}' has no primary key`);
|
|
127
|
-
}
|
|
163
|
+
if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
|
|
128
164
|
return this.findOne({ [this._primaryKey]: id });
|
|
129
165
|
}
|
|
130
166
|
|
|
131
|
-
/**
|
|
132
|
-
* 获取所有行
|
|
133
|
-
*/
|
|
134
167
|
findAll() {
|
|
135
|
-
return this.
|
|
168
|
+
return this._getVisibleRows().map(r => ({ ...r }));
|
|
136
169
|
}
|
|
137
170
|
|
|
138
|
-
/**
|
|
139
|
-
* 计数
|
|
140
|
-
*/
|
|
141
171
|
count(query = {}) {
|
|
142
|
-
if (Object.keys(query).length === 0)
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
return this._applyFilter(this._rows, query).length;
|
|
172
|
+
if (Object.keys(query).length === 0) return this._getVisibleRows().length;
|
|
173
|
+
return this._applyFilter(this._getVisibleRows(), query).length;
|
|
146
174
|
}
|
|
147
175
|
|
|
148
|
-
// ============================================================
|
|
149
|
-
// 链式查询
|
|
150
|
-
// ============================================================
|
|
151
|
-
|
|
152
176
|
where(conditions) {
|
|
153
177
|
return new Query(this, conditions);
|
|
154
178
|
}
|
|
@@ -158,12 +182,11 @@ class Table {
|
|
|
158
182
|
// ============================================================
|
|
159
183
|
|
|
160
184
|
update(query, updates) {
|
|
161
|
-
// 触发 beforeUpdate 钩子
|
|
162
185
|
for (const hook of this._hooks.beforeUpdate) {
|
|
163
186
|
updates = hook(query, { ...updates }) || updates;
|
|
164
187
|
}
|
|
165
188
|
|
|
166
|
-
const matched = this._applyFilter(this.
|
|
189
|
+
const matched = this._applyFilter(this._getVisibleRows(), query);
|
|
167
190
|
let count = 0;
|
|
168
191
|
|
|
169
192
|
for (const row of matched) {
|
|
@@ -172,7 +195,7 @@ class Table {
|
|
|
172
195
|
|
|
173
196
|
const oldData = { ...this._rows[index] };
|
|
174
197
|
|
|
175
|
-
//
|
|
198
|
+
// 唯一约束
|
|
176
199
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
177
200
|
if (def.unique && updates[field] !== undefined) {
|
|
178
201
|
for (let i = 0; i < this._rows.length; i++) {
|
|
@@ -183,32 +206,33 @@ class Table {
|
|
|
183
206
|
}
|
|
184
207
|
}
|
|
185
208
|
|
|
186
|
-
//
|
|
187
|
-
|
|
209
|
+
// CHECK 约束
|
|
210
|
+
const merged = { ...this._rows[index], ...updates };
|
|
211
|
+
for (const { field, fn } of this._checkConstraints) {
|
|
212
|
+
if (merged[field] !== undefined && !fn(merged[field], merged)) {
|
|
213
|
+
throw new Error(`CHECK constraint failed on '${this._name}.${field}'`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
188
216
|
|
|
189
|
-
|
|
217
|
+
Object.assign(this._rows[index], updates);
|
|
190
218
|
this._removeFromIndexes(index, oldData);
|
|
191
219
|
this._updateIndexes(index, this._rows[index]);
|
|
192
|
-
|
|
193
220
|
count++;
|
|
194
221
|
}
|
|
195
222
|
|
|
196
223
|
if (count > 0) {
|
|
197
224
|
this._dirty = true;
|
|
198
225
|
this._db._markDirty();
|
|
199
|
-
|
|
200
|
-
// 触发 afterUpdate 钩子
|
|
201
226
|
for (const hook of this._hooks.afterUpdate) {
|
|
202
227
|
hook(query, updates, count);
|
|
203
228
|
}
|
|
229
|
+
this._db._emitChange('update', this._name, { query, updates, count });
|
|
204
230
|
}
|
|
205
231
|
return count;
|
|
206
232
|
}
|
|
207
233
|
|
|
208
234
|
updateById(id, updates) {
|
|
209
|
-
if (!this._primaryKey) {
|
|
210
|
-
throw new Error(`Table '${this._name}' has no primary key`);
|
|
211
|
-
}
|
|
235
|
+
if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
|
|
212
236
|
return this.update({ [this._primaryKey]: id }, updates);
|
|
213
237
|
}
|
|
214
238
|
|
|
@@ -217,65 +241,192 @@ class Table {
|
|
|
217
241
|
// ============================================================
|
|
218
242
|
|
|
219
243
|
remove(query = {}) {
|
|
220
|
-
// 触发 beforeRemove 钩子
|
|
221
244
|
for (const hook of this._hooks.beforeRemove) {
|
|
222
|
-
|
|
223
|
-
if (result === false) return 0; // 钩子取消删除
|
|
245
|
+
if (hook(query) === false) return 0;
|
|
224
246
|
}
|
|
225
247
|
|
|
226
248
|
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;
|
|
249
|
+
return this._truncate();
|
|
235
250
|
}
|
|
236
251
|
|
|
237
|
-
const toRemove = this._applyFilter(this.
|
|
252
|
+
const toRemove = this._applyFilter(this._getVisibleRows(), query);
|
|
253
|
+
let count = 0;
|
|
254
|
+
|
|
238
255
|
for (const row of toRemove) {
|
|
239
256
|
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
|
-
}
|
|
257
|
+
if (index === -1) continue;
|
|
258
|
+
|
|
259
|
+
// 外键级联删除
|
|
260
|
+
for (const fk of this._foreignKeys) {
|
|
261
|
+
if (fk.onDelete === 'cascade') {
|
|
262
|
+
const refTable = this._db._tables[fk.table];
|
|
263
|
+
if (refTable) {
|
|
264
|
+
refTable.remove({ [fk.field]: row[this._primaryKey] });
|
|
255
265
|
}
|
|
256
266
|
}
|
|
257
267
|
}
|
|
268
|
+
|
|
269
|
+
if (this._softDelete) {
|
|
270
|
+
this._rows[index]._deleted = true;
|
|
271
|
+
this._rows[index]._deletedAt = Date.now();
|
|
272
|
+
} else {
|
|
273
|
+
this._removeFromIndexes(index, row);
|
|
274
|
+
this._rows.splice(index, 1);
|
|
275
|
+
this._adjustIndexesAfterRemove(index);
|
|
276
|
+
}
|
|
277
|
+
count++;
|
|
258
278
|
}
|
|
259
279
|
|
|
260
|
-
if (
|
|
280
|
+
if (count > 0) {
|
|
261
281
|
this._dirty = true;
|
|
262
282
|
this._db._markDirty();
|
|
263
|
-
|
|
264
|
-
// 触发 afterRemove 钩子
|
|
265
283
|
for (const hook of this._hooks.afterRemove) {
|
|
266
|
-
hook(query,
|
|
284
|
+
hook(query, count);
|
|
267
285
|
}
|
|
286
|
+
this._db._emitChange('remove', this._name, { query, count });
|
|
268
287
|
}
|
|
269
|
-
return
|
|
288
|
+
return count;
|
|
270
289
|
}
|
|
271
290
|
|
|
272
291
|
removeById(id) {
|
|
273
|
-
if (!this._primaryKey) {
|
|
274
|
-
throw new Error(`Table '${this._name}' has no primary key`);
|
|
275
|
-
}
|
|
292
|
+
if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
|
|
276
293
|
return this.remove({ [this._primaryKey]: id });
|
|
277
294
|
}
|
|
278
295
|
|
|
296
|
+
/**
|
|
297
|
+
* 软删除 — 标记为已删除,可恢复
|
|
298
|
+
*/
|
|
299
|
+
removeSoft(query = {}) {
|
|
300
|
+
this._softDelete = true;
|
|
301
|
+
const result = this.remove(query);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 恢复软删除的行
|
|
307
|
+
*/
|
|
308
|
+
restore(query = {}) {
|
|
309
|
+
if (!this._softDelete) throw new Error('Soft delete is not enabled');
|
|
310
|
+
const matched = this._applyFilter(this._rows, { ...query, _deleted: true });
|
|
311
|
+
for (const row of matched) {
|
|
312
|
+
const index = this._rows.indexOf(row);
|
|
313
|
+
if (index !== -1) {
|
|
314
|
+
delete this._rows[index]._deleted;
|
|
315
|
+
delete this._rows[index]._deletedAt;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return matched.length;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 清空表(保留结构)
|
|
323
|
+
*/
|
|
324
|
+
truncate() {
|
|
325
|
+
return this._truncate();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_truncate() {
|
|
329
|
+
const count = this._rows.length;
|
|
330
|
+
this._rows = [];
|
|
331
|
+
this._indexes = {};
|
|
332
|
+
this._autoIncrement = 0;
|
|
333
|
+
this._dirty = true;
|
|
334
|
+
this._db._markDirty();
|
|
335
|
+
return count;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ============================================================
|
|
339
|
+
// ALTER TABLE
|
|
340
|
+
// ============================================================
|
|
341
|
+
|
|
342
|
+
addColumn(field, definition) {
|
|
343
|
+
if (this._schema[field]) throw new Error(`Column '${field}' already exists`);
|
|
344
|
+
this._schema[field] = definition;
|
|
345
|
+
if (definition.default !== undefined) {
|
|
346
|
+
this._defaults[field] = definition.default;
|
|
347
|
+
// 填充现有行的默认值
|
|
348
|
+
for (const row of this._rows) {
|
|
349
|
+
if (row[field] === undefined) {
|
|
350
|
+
row[field] = typeof definition.default === 'function' ? definition.default() : definition.default;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (definition.check) this._checkConstraints.push({ field, fn: definition.check });
|
|
355
|
+
if (definition.foreignKey) {
|
|
356
|
+
this._foreignKeys.push({
|
|
357
|
+
field,
|
|
358
|
+
table: definition.foreignKey.table,
|
|
359
|
+
onDelete: definition.foreignKey.onDelete || 'restrict'
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (definition.computed) this._computedFields[field] = definition.computed;
|
|
363
|
+
this._dirty = true;
|
|
364
|
+
this._db._markDirty();
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
dropColumn(field) {
|
|
369
|
+
if (!this._schema[field]) throw new Error(`Column '${field}' does not exist`);
|
|
370
|
+
delete this._schema[field];
|
|
371
|
+
delete this._defaults[field];
|
|
372
|
+
this._checkConstraints = this._checkConstraints.filter(c => c.field !== field);
|
|
373
|
+
this._foreignKeys = this._foreignKeys.filter(fk => fk.field !== field);
|
|
374
|
+
delete this._computedFields[field];
|
|
375
|
+
// 移除所有行中的该字段
|
|
376
|
+
for (const row of this._rows) {
|
|
377
|
+
delete row[field];
|
|
378
|
+
}
|
|
379
|
+
this._dirty = true;
|
|
380
|
+
this._db._markDirty();
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
renameColumn(oldName, newName) {
|
|
385
|
+
if (!this._schema[oldName]) throw new Error(`Column '${oldName}' does not exist`);
|
|
386
|
+
if (this._schema[newName]) throw new Error(`Column '${newName}' already exists`);
|
|
387
|
+
this._schema[newName] = this._schema[oldName];
|
|
388
|
+
delete this._schema[oldName];
|
|
389
|
+
if (this._defaults[oldName] !== undefined) {
|
|
390
|
+
this._defaults[newName] = this._defaults[oldName];
|
|
391
|
+
delete this._defaults[oldName];
|
|
392
|
+
}
|
|
393
|
+
for (const row of this._rows) {
|
|
394
|
+
row[newName] = row[oldName];
|
|
395
|
+
delete row[oldName];
|
|
396
|
+
}
|
|
397
|
+
if (this._primaryKey === oldName) this._primaryKey = newName;
|
|
398
|
+
if (this._autoIncrementField === oldName) this._autoIncrementField = newName;
|
|
399
|
+
this._dirty = true;
|
|
400
|
+
this._db._markDirty();
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
modifyColumn(field, definition) {
|
|
405
|
+
if (!this._schema[field]) throw new Error(`Column '${field}' does not exist`);
|
|
406
|
+
this._schema[field] = { ...this._schema[field], ...definition };
|
|
407
|
+
if (definition.default !== undefined) this._defaults[field] = definition.default;
|
|
408
|
+
this._checkConstraints = this._checkConstraints.filter(c => c.field !== field);
|
|
409
|
+
if (definition.check) this._checkConstraints.push({ field, fn: definition.check });
|
|
410
|
+
this._dirty = true;
|
|
411
|
+
this._db._markDirty();
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 获取表结构
|
|
417
|
+
*/
|
|
418
|
+
describe() {
|
|
419
|
+
return {
|
|
420
|
+
name: this._name,
|
|
421
|
+
columns: { ...this._schema },
|
|
422
|
+
primaryKey: this._primaryKey,
|
|
423
|
+
rowCount: this._getVisibleRows().length,
|
|
424
|
+
indexCount: Object.keys(this._indexes).length,
|
|
425
|
+
softDelete: this._softDelete,
|
|
426
|
+
foreignKeys: this._foreignKeys.map(fk => ({ field: fk.field, table: fk.table }))
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
279
430
|
// ============================================================
|
|
280
431
|
// 索引
|
|
281
432
|
// ============================================================
|
|
@@ -302,17 +453,6 @@ class Table {
|
|
|
302
453
|
// 事件钩子
|
|
303
454
|
// ============================================================
|
|
304
455
|
|
|
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
456
|
on(event, callback) {
|
|
317
457
|
if (this._hooks[event]) {
|
|
318
458
|
this._hooks[event].push(callback);
|
|
@@ -328,7 +468,7 @@ class Table {
|
|
|
328
468
|
}
|
|
329
469
|
|
|
330
470
|
// ============================================================
|
|
331
|
-
//
|
|
471
|
+
// 内部: 过滤器
|
|
332
472
|
// ============================================================
|
|
333
473
|
|
|
334
474
|
_applyFilter(rows, query) {
|
|
@@ -343,20 +483,43 @@ class Table {
|
|
|
343
483
|
|
|
344
484
|
_matchRow(row, query) {
|
|
345
485
|
for (const [field, condition] of Object.entries(query)) {
|
|
346
|
-
|
|
486
|
+
// 顶层 $or / $and / $not
|
|
487
|
+
if (field === '$or') {
|
|
488
|
+
if (!Array.isArray(condition) || !condition.some(sub => this._matchRow(row, sub))) return false;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (field === '$and') {
|
|
492
|
+
if (!Array.isArray(condition) || !condition.every(sub => this._matchRow(row, sub))) return false;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (field === '$not') {
|
|
496
|
+
if (this._matchRow(row, condition)) return false;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
347
499
|
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
500
|
+
// JSON 路径: 'profile.city' 或 'data.profile.city'
|
|
501
|
+
const value = this._resolvePath(row, field);
|
|
502
|
+
|
|
503
|
+
if (condition !== null && typeof condition === 'object' && !Array.isArray(condition) && !(condition instanceof RegExp)) {
|
|
504
|
+
// 检查是否是操作符
|
|
505
|
+
const keys = Object.keys(condition);
|
|
506
|
+
const isOperator = keys.every(k => k.startsWith('$'));
|
|
507
|
+
if (isOperator && keys.length > 0) {
|
|
508
|
+
for (const [op, target] of Object.entries(condition)) {
|
|
509
|
+
if (!this._matchOperator(value, op, target)) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
// 普通对象:递归匹配子文档
|
|
515
|
+
if (value !== null && typeof value === 'object') {
|
|
516
|
+
if (!this._matchRow(value, condition)) return false;
|
|
517
|
+
} else {
|
|
352
518
|
return false;
|
|
353
519
|
}
|
|
354
520
|
}
|
|
355
521
|
} else {
|
|
356
|
-
|
|
357
|
-
if (value !== condition) {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
522
|
+
if (value !== condition) return false;
|
|
360
523
|
}
|
|
361
524
|
}
|
|
362
525
|
return true;
|
|
@@ -374,8 +537,8 @@ class Table {
|
|
|
374
537
|
case '$nin': return Array.isArray(target) && !target.includes(value);
|
|
375
538
|
case '$like':
|
|
376
539
|
if (typeof value !== 'string' || typeof target !== 'string') return false;
|
|
377
|
-
const
|
|
378
|
-
return
|
|
540
|
+
const likeRegex = target.replace(/%/g, '.*').replace(/_/g, '.');
|
|
541
|
+
return new RegExp('^' + likeRegex + '$', 'i').test(value);
|
|
379
542
|
case '$regex':
|
|
380
543
|
return new RegExp(target).test(String(value));
|
|
381
544
|
case '$exists':
|
|
@@ -383,13 +546,82 @@ class Table {
|
|
|
383
546
|
case '$between':
|
|
384
547
|
return Array.isArray(target) && target.length === 2
|
|
385
548
|
&& value >= target[0] && value <= target[1];
|
|
549
|
+
case '$and':
|
|
550
|
+
return Array.isArray(target) && target.every(sub => this._matchRow({ _val: value }, { _val: sub }));
|
|
386
551
|
case '$or':
|
|
387
|
-
return Array.isArray(target) && target.some(sub => this._matchRow({
|
|
552
|
+
return Array.isArray(target) && target.some(sub => this._matchRow({ _val: value }, { _val: sub }));
|
|
553
|
+
case '$not':
|
|
554
|
+
return !this._matchRow({ _val: value }, { _val: target });
|
|
555
|
+
case '$contains':
|
|
556
|
+
return Array.isArray(value) && value.includes(target);
|
|
557
|
+
case '$type':
|
|
558
|
+
return typeof value === target;
|
|
559
|
+
case '$size':
|
|
560
|
+
return Array.isArray(value) && value.length === target;
|
|
561
|
+
case '$elemMatch':
|
|
562
|
+
return Array.isArray(value) && value.some(el =>
|
|
563
|
+
typeof el === 'object' ? this._matchRow(el, target) : el === target
|
|
564
|
+
);
|
|
565
|
+
case '$isNull':
|
|
566
|
+
return target ? value === null || value === undefined : value !== null && value !== undefined;
|
|
567
|
+
case '$startsWith':
|
|
568
|
+
return typeof value === 'string' && value.startsWith(target);
|
|
569
|
+
case '$endsWith':
|
|
570
|
+
return typeof value === 'string' && value.endsWith(target);
|
|
571
|
+
case '$search':
|
|
572
|
+
// 全文搜索(简单实现)
|
|
573
|
+
if (typeof value !== 'string') return false;
|
|
574
|
+
const terms = Array.isArray(target) ? target : [target];
|
|
575
|
+
return terms.every(t => value.toLowerCase().includes(t.toLowerCase()));
|
|
388
576
|
default:
|
|
389
577
|
return false;
|
|
390
578
|
}
|
|
391
579
|
}
|
|
392
580
|
|
|
581
|
+
/**
|
|
582
|
+
* 解析 JSON 路径,如 'profile.city' -> row.profile.city
|
|
583
|
+
*/
|
|
584
|
+
_resolvePath(row, field) {
|
|
585
|
+
if (field.includes('.') && !field.startsWith('$')) {
|
|
586
|
+
const parts = field.split('.');
|
|
587
|
+
let value = row;
|
|
588
|
+
for (const part of parts) {
|
|
589
|
+
if (value === null || value === undefined) return undefined;
|
|
590
|
+
value = value[part];
|
|
591
|
+
}
|
|
592
|
+
return value;
|
|
593
|
+
}
|
|
594
|
+
return row[field];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ============================================================
|
|
598
|
+
// 内部: 辅助
|
|
599
|
+
// ============================================================
|
|
600
|
+
|
|
601
|
+
_applyDefaults(data) {
|
|
602
|
+
const result = { ...data };
|
|
603
|
+
for (const [field, defaultValue] of Object.entries(this._defaults)) {
|
|
604
|
+
if (result[field] === undefined) {
|
|
605
|
+
result[field] = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
_applyComputed(data) {
|
|
612
|
+
for (const [field, fn] of Object.entries(this._computedFields)) {
|
|
613
|
+
data[field] = fn(data);
|
|
614
|
+
}
|
|
615
|
+
return data;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
_getVisibleRows() {
|
|
619
|
+
if (this._softDelete) {
|
|
620
|
+
return this._rows.filter(r => !r._deleted);
|
|
621
|
+
}
|
|
622
|
+
return this._rows;
|
|
623
|
+
}
|
|
624
|
+
|
|
393
625
|
_updateIndexes(rowIndex, data) {
|
|
394
626
|
for (const field of Object.keys(this._indexes)) {
|
|
395
627
|
const value = data[field];
|
|
@@ -416,9 +648,24 @@ class Table {
|
|
|
416
648
|
}
|
|
417
649
|
}
|
|
418
650
|
|
|
651
|
+
_adjustIndexesAfterRemove(removedIndex) {
|
|
652
|
+
for (const idxName of Object.keys(this._indexes)) {
|
|
653
|
+
const idx = this._indexes[idxName];
|
|
654
|
+
for (const [key, positions] of idx) {
|
|
655
|
+
const newPositions = positions
|
|
656
|
+
.filter(p => p !== removedIndex)
|
|
657
|
+
.map(p => p > removedIndex ? p - 1 : p);
|
|
658
|
+
if (newPositions.length === 0) {
|
|
659
|
+
idx.delete(key);
|
|
660
|
+
} else {
|
|
661
|
+
idx.set(key, newPositions);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
419
667
|
_loadRows(rows) {
|
|
420
668
|
this._rows = rows;
|
|
421
|
-
// 重建自增
|
|
422
669
|
if (this._autoIncrementField) {
|
|
423
670
|
for (const row of rows) {
|
|
424
671
|
const val = row[this._autoIncrementField];
|
|
@@ -427,183 +674,14 @@ class Table {
|
|
|
427
674
|
}
|
|
428
675
|
}
|
|
429
676
|
}
|
|
430
|
-
// 重建所有索引
|
|
431
677
|
for (const field of Object.keys(this._indexes)) {
|
|
432
678
|
this.createIndex(field);
|
|
433
679
|
}
|
|
434
680
|
}
|
|
435
681
|
|
|
436
|
-
// ============================================================
|
|
437
|
-
// 导出
|
|
438
|
-
// ============================================================
|
|
439
|
-
|
|
440
682
|
toJSON() {
|
|
441
683
|
return this._rows;
|
|
442
684
|
}
|
|
443
685
|
}
|
|
444
686
|
|
|
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
687
|
module.exports = Table;
|