jsql-neo 1.0.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 +316 -58
- package/lib/query.js +435 -0
- package/lib/table.js +391 -199
- package/package.json +1 -1
package/lib/table.js
CHANGED
|
@@ -1,28 +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
|
-
|
|
17
|
-
|
|
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: []
|
|
22
|
+
};
|
|
23
|
+
this._softDelete = false; // 是否启用软删除
|
|
24
|
+
this._computedFields = {}; // 计算字段
|
|
25
|
+
|
|
26
|
+
// 解析列定义
|
|
18
27
|
this._primaryKey = null;
|
|
19
28
|
this._autoIncrementField = null;
|
|
29
|
+
this._foreignKeys = [];
|
|
30
|
+
this._checkConstraints = [];
|
|
31
|
+
this._defaults = {};
|
|
32
|
+
|
|
20
33
|
for (const [field, def] of Object.entries(schema)) {
|
|
21
34
|
if (def.primaryKey) this._primaryKey = field;
|
|
22
35
|
if (def.autoIncrement) {
|
|
23
36
|
this._autoIncrementField = field;
|
|
24
37
|
if (def.primaryKey) this._primaryKey = field;
|
|
25
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;
|
|
26
59
|
}
|
|
27
60
|
}
|
|
28
61
|
|
|
@@ -31,14 +64,32 @@ class Table {
|
|
|
31
64
|
// ============================================================
|
|
32
65
|
|
|
33
66
|
insert(data) {
|
|
34
|
-
|
|
67
|
+
for (const hook of this._hooks.beforeInsert) {
|
|
68
|
+
data = hook({ ...data }) || data;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 填充默认值
|
|
72
|
+
data = this._applyDefaults(data);
|
|
73
|
+
|
|
74
|
+
// 计算字段
|
|
75
|
+
data = this._applyComputed(data);
|
|
76
|
+
|
|
77
|
+
// 校验必填
|
|
35
78
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
79
|
+
if (field === '_softDelete') continue;
|
|
36
80
|
if (def.required && (data[field] === undefined || data[field] === null)) {
|
|
37
81
|
throw new Error(`Field '${field}' is required in table '${this._name}'`);
|
|
38
82
|
}
|
|
39
83
|
}
|
|
40
84
|
|
|
41
|
-
//
|
|
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
|
+
// 唯一约束
|
|
42
93
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
43
94
|
if (def.unique && data[field] !== undefined) {
|
|
44
95
|
for (const row of this._rows) {
|
|
@@ -50,23 +101,23 @@ class Table {
|
|
|
50
101
|
}
|
|
51
102
|
|
|
52
103
|
// 自增
|
|
53
|
-
if (this._autoIncrementField) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this._autoIncrement = data[this._autoIncrementField];
|
|
59
|
-
}
|
|
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];
|
|
60
109
|
}
|
|
61
110
|
|
|
62
111
|
const rowIndex = this._rows.length;
|
|
63
112
|
this._rows.push({ ...data });
|
|
64
|
-
|
|
65
|
-
// 更新索引
|
|
66
113
|
this._updateIndexes(rowIndex, data);
|
|
67
|
-
|
|
68
114
|
this._dirty = true;
|
|
69
115
|
this._db._markDirty();
|
|
116
|
+
|
|
117
|
+
for (const hook of this._hooks.afterInsert) {
|
|
118
|
+
hook(data);
|
|
119
|
+
}
|
|
120
|
+
|
|
70
121
|
return data;
|
|
71
122
|
}
|
|
72
123
|
|
|
@@ -74,62 +125,52 @@ class Table {
|
|
|
74
125
|
return items.map(item => this.insert(item));
|
|
75
126
|
}
|
|
76
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
|
+
|
|
77
143
|
// ============================================================
|
|
78
144
|
// 查询
|
|
79
145
|
// ============================================================
|
|
80
146
|
|
|
81
|
-
/**
|
|
82
|
-
* 查找所有匹配的行
|
|
83
|
-
*/
|
|
84
147
|
find(query = {}) {
|
|
85
|
-
let rows = this.
|
|
86
|
-
|
|
148
|
+
let rows = this._getVisibleRows();
|
|
87
149
|
if (Object.keys(query).length > 0) {
|
|
88
150
|
rows = this._applyFilter(rows, query);
|
|
89
151
|
}
|
|
90
|
-
|
|
91
152
|
return rows.map(r => ({ ...r }));
|
|
92
153
|
}
|
|
93
154
|
|
|
94
|
-
/**
|
|
95
|
-
* 查找第一条匹配的行
|
|
96
|
-
*/
|
|
97
155
|
findOne(query = {}) {
|
|
98
156
|
const rows = this.find(query);
|
|
99
157
|
return rows.length > 0 ? rows[0] : null;
|
|
100
158
|
}
|
|
101
159
|
|
|
102
|
-
/**
|
|
103
|
-
* 按主键查找
|
|
104
|
-
*/
|
|
105
160
|
findById(id) {
|
|
106
|
-
if (!this._primaryKey) {
|
|
107
|
-
throw new Error(`Table '${this._name}' has no primary key`);
|
|
108
|
-
}
|
|
161
|
+
if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
|
|
109
162
|
return this.findOne({ [this._primaryKey]: id });
|
|
110
163
|
}
|
|
111
164
|
|
|
112
|
-
/**
|
|
113
|
-
* 获取所有行
|
|
114
|
-
*/
|
|
115
165
|
findAll() {
|
|
116
|
-
return this.
|
|
166
|
+
return this._getVisibleRows().map(r => ({ ...r }));
|
|
117
167
|
}
|
|
118
168
|
|
|
119
|
-
/**
|
|
120
|
-
* 计数
|
|
121
|
-
*/
|
|
122
169
|
count(query = {}) {
|
|
123
|
-
if (Object.keys(query).length === 0)
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
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;
|
|
127
172
|
}
|
|
128
173
|
|
|
129
|
-
// ============================================================
|
|
130
|
-
// 链式查询
|
|
131
|
-
// ============================================================
|
|
132
|
-
|
|
133
174
|
where(conditions) {
|
|
134
175
|
return new Query(this, conditions);
|
|
135
176
|
}
|
|
@@ -139,7 +180,11 @@ class Table {
|
|
|
139
180
|
// ============================================================
|
|
140
181
|
|
|
141
182
|
update(query, updates) {
|
|
142
|
-
const
|
|
183
|
+
for (const hook of this._hooks.beforeUpdate) {
|
|
184
|
+
updates = hook(query, { ...updates }) || updates;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const matched = this._applyFilter(this._getVisibleRows(), query);
|
|
143
188
|
let count = 0;
|
|
144
189
|
|
|
145
190
|
for (const row of matched) {
|
|
@@ -148,7 +193,7 @@ class Table {
|
|
|
148
193
|
|
|
149
194
|
const oldData = { ...this._rows[index] };
|
|
150
195
|
|
|
151
|
-
//
|
|
196
|
+
// 唯一约束
|
|
152
197
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
153
198
|
if (def.unique && updates[field] !== undefined) {
|
|
154
199
|
for (let i = 0; i < this._rows.length; i++) {
|
|
@@ -159,27 +204,32 @@ class Table {
|
|
|
159
204
|
}
|
|
160
205
|
}
|
|
161
206
|
|
|
162
|
-
//
|
|
163
|
-
|
|
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
|
+
}
|
|
164
214
|
|
|
165
|
-
|
|
215
|
+
Object.assign(this._rows[index], updates);
|
|
166
216
|
this._removeFromIndexes(index, oldData);
|
|
167
217
|
this._updateIndexes(index, this._rows[index]);
|
|
168
|
-
|
|
169
218
|
count++;
|
|
170
219
|
}
|
|
171
220
|
|
|
172
221
|
if (count > 0) {
|
|
173
222
|
this._dirty = true;
|
|
174
223
|
this._db._markDirty();
|
|
224
|
+
for (const hook of this._hooks.afterUpdate) {
|
|
225
|
+
hook(query, updates, count);
|
|
226
|
+
}
|
|
175
227
|
}
|
|
176
228
|
return count;
|
|
177
229
|
}
|
|
178
230
|
|
|
179
231
|
updateById(id, updates) {
|
|
180
|
-
if (!this._primaryKey) {
|
|
181
|
-
throw new Error(`Table '${this._name}' has no primary key`);
|
|
182
|
-
}
|
|
232
|
+
if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
|
|
183
233
|
return this.update({ [this._primaryKey]: id }, updates);
|
|
184
234
|
}
|
|
185
235
|
|
|
@@ -188,54 +238,183 @@ class Table {
|
|
|
188
238
|
// ============================================================
|
|
189
239
|
|
|
190
240
|
remove(query = {}) {
|
|
241
|
+
for (const hook of this._hooks.beforeRemove) {
|
|
242
|
+
if (hook(query) === false) return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
191
245
|
if (Object.keys(query).length === 0) {
|
|
192
|
-
|
|
193
|
-
const count = this._rows.length;
|
|
194
|
-
this._rows = [];
|
|
195
|
-
this._indexes = {};
|
|
196
|
-
this._autoIncrement = 0;
|
|
197
|
-
this._dirty = true;
|
|
198
|
-
this._db._markDirty();
|
|
199
|
-
return count;
|
|
246
|
+
return this._truncate();
|
|
200
247
|
}
|
|
201
248
|
|
|
202
|
-
const toRemove = this._applyFilter(this.
|
|
249
|
+
const toRemove = this._applyFilter(this._getVisibleRows(), query);
|
|
250
|
+
let count = 0;
|
|
251
|
+
|
|
203
252
|
for (const row of toRemove) {
|
|
204
253
|
const index = this._rows.indexOf(row);
|
|
205
|
-
if (index
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
.filter(p => p !== index)
|
|
214
|
-
.map(p => p > index ? p - 1 : p);
|
|
215
|
-
if (newPositions.length === 0) {
|
|
216
|
-
idx.delete(key);
|
|
217
|
-
} else {
|
|
218
|
-
idx.set(key, newPositions);
|
|
219
|
-
}
|
|
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] });
|
|
220
262
|
}
|
|
221
263
|
}
|
|
222
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++;
|
|
223
275
|
}
|
|
224
276
|
|
|
225
|
-
if (
|
|
277
|
+
if (count > 0) {
|
|
226
278
|
this._dirty = true;
|
|
227
279
|
this._db._markDirty();
|
|
280
|
+
for (const hook of this._hooks.afterRemove) {
|
|
281
|
+
hook(query, count);
|
|
282
|
+
}
|
|
228
283
|
}
|
|
229
|
-
return
|
|
284
|
+
return count;
|
|
230
285
|
}
|
|
231
286
|
|
|
232
287
|
removeById(id) {
|
|
233
|
-
if (!this._primaryKey) {
|
|
234
|
-
throw new Error(`Table '${this._name}' has no primary key`);
|
|
235
|
-
}
|
|
288
|
+
if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
|
|
236
289
|
return this.remove({ [this._primaryKey]: id });
|
|
237
290
|
}
|
|
238
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
|
+
|
|
239
418
|
// ============================================================
|
|
240
419
|
// 索引
|
|
241
420
|
// ============================================================
|
|
@@ -259,7 +438,25 @@ class Table {
|
|
|
259
438
|
}
|
|
260
439
|
|
|
261
440
|
// ============================================================
|
|
262
|
-
//
|
|
441
|
+
// 事件钩子
|
|
442
|
+
// ============================================================
|
|
443
|
+
|
|
444
|
+
on(event, callback) {
|
|
445
|
+
if (this._hooks[event]) {
|
|
446
|
+
this._hooks[event].push(callback);
|
|
447
|
+
}
|
|
448
|
+
return this;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
off(event, callback) {
|
|
452
|
+
if (this._hooks[event]) {
|
|
453
|
+
this._hooks[event] = this._hooks[event].filter(cb => cb !== callback);
|
|
454
|
+
}
|
|
455
|
+
return this;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ============================================================
|
|
459
|
+
// 内部: 过滤器
|
|
263
460
|
// ============================================================
|
|
264
461
|
|
|
265
462
|
_applyFilter(rows, query) {
|
|
@@ -274,20 +471,29 @@ class Table {
|
|
|
274
471
|
|
|
275
472
|
_matchRow(row, query) {
|
|
276
473
|
for (const [field, condition] of Object.entries(query)) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (condition !== null && typeof condition === 'object' && !Array.isArray(condition)) {
|
|
281
|
-
|
|
282
|
-
|
|
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 {
|
|
283
492
|
return false;
|
|
284
493
|
}
|
|
285
494
|
}
|
|
286
495
|
} else {
|
|
287
|
-
|
|
288
|
-
if (value !== condition) {
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
496
|
+
if (value !== condition) return false;
|
|
291
497
|
}
|
|
292
498
|
}
|
|
293
499
|
return true;
|
|
@@ -305,8 +511,7 @@ class Table {
|
|
|
305
511
|
case '$nin': return Array.isArray(target) && !target.includes(value);
|
|
306
512
|
case '$like':
|
|
307
513
|
if (typeof value !== 'string' || typeof target !== 'string') return false;
|
|
308
|
-
|
|
309
|
-
return regex.test(value);
|
|
514
|
+
return new RegExp(target.replace(/%/g, '.*').replace(/_/g, '.'), 'i').test(value);
|
|
310
515
|
case '$regex':
|
|
311
516
|
return new RegExp(target).test(String(value));
|
|
312
517
|
case '$exists':
|
|
@@ -314,13 +519,82 @@ class Table {
|
|
|
314
519
|
case '$between':
|
|
315
520
|
return Array.isArray(target) && target.length === 2
|
|
316
521
|
&& value >= target[0] && value <= target[1];
|
|
522
|
+
case '$and':
|
|
523
|
+
return Array.isArray(target) && target.every(sub => this._matchRow({ _val: value }, { _val: sub }));
|
|
317
524
|
case '$or':
|
|
318
|
-
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()));
|
|
319
549
|
default:
|
|
320
550
|
return false;
|
|
321
551
|
}
|
|
322
552
|
}
|
|
323
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
|
+
|
|
324
598
|
_updateIndexes(rowIndex, data) {
|
|
325
599
|
for (const field of Object.keys(this._indexes)) {
|
|
326
600
|
const value = data[field];
|
|
@@ -347,9 +621,24 @@ class Table {
|
|
|
347
621
|
}
|
|
348
622
|
}
|
|
349
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
|
+
|
|
350
640
|
_loadRows(rows) {
|
|
351
641
|
this._rows = rows;
|
|
352
|
-
// 重建自增
|
|
353
642
|
if (this._autoIncrementField) {
|
|
354
643
|
for (const row of rows) {
|
|
355
644
|
const val = row[this._autoIncrementField];
|
|
@@ -358,111 +647,14 @@ class Table {
|
|
|
358
647
|
}
|
|
359
648
|
}
|
|
360
649
|
}
|
|
361
|
-
// 重建所有索引
|
|
362
650
|
for (const field of Object.keys(this._indexes)) {
|
|
363
651
|
this.createIndex(field);
|
|
364
652
|
}
|
|
365
653
|
}
|
|
366
654
|
|
|
367
|
-
// ============================================================
|
|
368
|
-
// 导出
|
|
369
|
-
// ============================================================
|
|
370
|
-
|
|
371
655
|
toJSON() {
|
|
372
656
|
return this._rows;
|
|
373
657
|
}
|
|
374
658
|
}
|
|
375
659
|
|
|
376
|
-
// ============================================================
|
|
377
|
-
// 链式查询构建器
|
|
378
|
-
// ============================================================
|
|
379
|
-
|
|
380
|
-
class Query {
|
|
381
|
-
constructor(table, conditions) {
|
|
382
|
-
this._table = table;
|
|
383
|
-
this._conditions = conditions;
|
|
384
|
-
this._orderBy = null;
|
|
385
|
-
this._orderDir = 'asc';
|
|
386
|
-
this._limitCount = null;
|
|
387
|
-
this._offsetCount = 0;
|
|
388
|
-
this._selectFields = null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
orderBy(field, dir = 'asc') {
|
|
392
|
-
this._orderBy = field;
|
|
393
|
-
this._orderDir = dir;
|
|
394
|
-
return this;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
limit(n) {
|
|
398
|
-
this._limitCount = n;
|
|
399
|
-
return this;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
offset(n) {
|
|
403
|
-
this._offsetCount = n;
|
|
404
|
-
return this;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
select(fields) {
|
|
408
|
-
this._selectFields = Array.isArray(fields) ? fields : [fields];
|
|
409
|
-
return this;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
get() {
|
|
413
|
-
let rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
414
|
-
|
|
415
|
-
// 排序
|
|
416
|
-
if (this._orderBy) {
|
|
417
|
-
rows = rows.sort((a, b) => {
|
|
418
|
-
const va = a[this._orderBy];
|
|
419
|
-
const vb = b[this._orderBy];
|
|
420
|
-
if (va < vb) return this._orderDir === 'asc' ? -1 : 1;
|
|
421
|
-
if (va > vb) return this._orderDir === 'asc' ? 1 : -1;
|
|
422
|
-
return 0;
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// 分页
|
|
427
|
-
if (this._offsetCount > 0) {
|
|
428
|
-
rows = rows.slice(this._offsetCount);
|
|
429
|
-
}
|
|
430
|
-
if (this._limitCount !== null) {
|
|
431
|
-
rows = rows.slice(0, this._limitCount);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// 字段选择
|
|
435
|
-
rows = rows.map(r => ({ ...r }));
|
|
436
|
-
if (this._selectFields) {
|
|
437
|
-
rows = rows.map(r => {
|
|
438
|
-
const obj = {};
|
|
439
|
-
for (const f of this._selectFields) {
|
|
440
|
-
obj[f] = r[f];
|
|
441
|
-
}
|
|
442
|
-
return obj;
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return rows;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
first() {
|
|
450
|
-
const rows = this.limit(1).get();
|
|
451
|
-
return rows.length > 0 ? rows[0] : null;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
count() {
|
|
455
|
-
const rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
456
|
-
return rows.length;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
update(updates) {
|
|
460
|
-
return this._table.update(this._conditions, updates);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
remove() {
|
|
464
|
-
return this._table.remove(this._conditions);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
660
|
module.exports = Table;
|