@yanit/jsondb 0.1.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/README.md +903 -0
- package/dist/bin/cli-export.d.ts +7 -0
- package/dist/bin/cli-export.d.ts.map +1 -0
- package/dist/bin/cli-export.js +318 -0
- package/dist/bin/cli-export.js.map +1 -0
- package/dist/bin/cli-import.d.ts +7 -0
- package/dist/bin/cli-import.d.ts.map +1 -0
- package/dist/bin/cli-import.js +298 -0
- package/dist/bin/cli-import.js.map +1 -0
- package/dist/bin/server.d.ts +7 -0
- package/dist/bin/server.d.ts.map +1 -0
- package/dist/bin/server.js +92 -0
- package/dist/bin/server.js.map +1 -0
- package/dist/examples/sql-example.d.ts +7 -0
- package/dist/examples/sql-example.d.ts.map +1 -0
- package/dist/examples/sql-example.js +131 -0
- package/dist/examples/sql-example.js.map +1 -0
- package/dist/src/BulkOp.d.ts +74 -0
- package/dist/src/BulkOp.d.ts.map +1 -0
- package/dist/src/BulkOp.js +143 -0
- package/dist/src/BulkOp.js.map +1 -0
- package/dist/src/Collection.d.ts +232 -0
- package/dist/src/Collection.d.ts.map +1 -0
- package/dist/src/Collection.js +705 -0
- package/dist/src/Collection.js.map +1 -0
- package/dist/src/Cursor.d.ts +94 -0
- package/dist/src/Cursor.d.ts.map +1 -0
- package/dist/src/Cursor.js +259 -0
- package/dist/src/Cursor.js.map +1 -0
- package/dist/src/Database.d.ts +98 -0
- package/dist/src/Database.d.ts.map +1 -0
- package/dist/src/Database.js +198 -0
- package/dist/src/Database.js.map +1 -0
- package/dist/src/Operators.d.ts +73 -0
- package/dist/src/Operators.d.ts.map +1 -0
- package/dist/src/Operators.js +339 -0
- package/dist/src/Operators.js.map +1 -0
- package/dist/src/QueryCache.d.ts +87 -0
- package/dist/src/QueryCache.d.ts.map +1 -0
- package/dist/src/QueryCache.js +155 -0
- package/dist/src/QueryCache.js.map +1 -0
- package/dist/src/SQLExecutor.d.ts +60 -0
- package/dist/src/SQLExecutor.d.ts.map +1 -0
- package/dist/src/SQLExecutor.js +317 -0
- package/dist/src/SQLExecutor.js.map +1 -0
- package/dist/src/SQLParser.d.ts +181 -0
- package/dist/src/SQLParser.d.ts.map +1 -0
- package/dist/src/SQLParser.js +640 -0
- package/dist/src/SQLParser.js.map +1 -0
- package/dist/src/Schema.d.ts +92 -0
- package/dist/src/Schema.d.ts.map +1 -0
- package/dist/src/Schema.js +253 -0
- package/dist/src/Schema.js.map +1 -0
- package/dist/src/Transaction.d.ts +118 -0
- package/dist/src/Transaction.d.ts.map +1 -0
- package/dist/src/Transaction.js +233 -0
- package/dist/src/Transaction.js.map +1 -0
- package/dist/src/Utils.d.ts +68 -0
- package/dist/src/Utils.d.ts.map +1 -0
- package/dist/src/Utils.js +187 -0
- package/dist/src/Utils.js.map +1 -0
- package/dist/src/errors.d.ts +58 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +85 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +39 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +44 -0
- package/dist/src/index.js.map +1 -0
- package/dist/test/basic.test.d.ts +5 -0
- package/dist/test/basic.test.d.ts.map +1 -0
- package/dist/test/basic.test.js +283 -0
- package/dist/test/basic.test.js.map +1 -0
- package/dist/test/index.test.d.ts +5 -0
- package/dist/test/index.test.d.ts.map +1 -0
- package/dist/test/index.test.js +126 -0
- package/dist/test/index.test.js.map +1 -0
- package/dist/test/jsonb.test.d.ts +5 -0
- package/dist/test/jsonb.test.d.ts.map +1 -0
- package/dist/test/jsonb.test.js +165 -0
- package/dist/test/jsonb.test.js.map +1 -0
- package/dist/test/optimization.test.d.ts +6 -0
- package/dist/test/optimization.test.d.ts.map +1 -0
- package/dist/test/optimization.test.js +196 -0
- package/dist/test/optimization.test.js.map +1 -0
- package/dist/test/schema.test.d.ts +5 -0
- package/dist/test/schema.test.d.ts.map +1 -0
- package/dist/test/schema.test.js +197 -0
- package/dist/test/schema.test.js.map +1 -0
- package/dist/test/sql.test.d.ts +7 -0
- package/dist/test/sql.test.d.ts.map +1 -0
- package/dist/test/sql.test.js +21 -0
- package/dist/test/sql.test.js.map +1 -0
- package/package.json +73 -0
- package/src/BulkOp.js +181 -0
- package/src/BulkOp.ts +191 -0
- package/src/Collection.js +843 -0
- package/src/Collection.ts +896 -0
- package/src/Cursor.js +315 -0
- package/src/Cursor.ts +319 -0
- package/src/Database.js +244 -0
- package/src/Database.ts +268 -0
- package/src/Operators.js +382 -0
- package/src/Operators.ts +375 -0
- package/src/QueryCache.js +190 -0
- package/src/QueryCache.ts +208 -0
- package/src/SQLExecutor.ts +391 -0
- package/src/SQLParser.ts +814 -0
- package/src/Schema.js +292 -0
- package/src/Schema.ts +317 -0
- package/src/Transaction.js +291 -0
- package/src/Transaction.ts +313 -0
- package/src/Utils.js +205 -0
- package/src/Utils.ts +205 -0
- package/src/errors.js +93 -0
- package/src/errors.ts +93 -0
- package/src/index.js +90 -0
- package/src/index.ts +106 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 集合类 - 管理文档集合
|
|
3
|
+
* 支持 async/await 异步操作
|
|
4
|
+
* 支持 JSONB 二进制存储模式
|
|
5
|
+
* 支持 Schema 验证
|
|
6
|
+
* 支持内存缓存优化
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { generateId, deepClone, getNestedValue } from './Utils.js';
|
|
13
|
+
import { Cursor } from './Cursor.js';
|
|
14
|
+
import { matchQuery, applyUpdate } from './Operators.js';
|
|
15
|
+
import { CollectionNotFoundError, DocumentNotFoundError, ValidationError } from './errors.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Collection 类
|
|
19
|
+
* 表示一个文档集合,提供 CRUD 操作
|
|
20
|
+
*/
|
|
21
|
+
export class Collection {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Database} db - 数据库实例
|
|
24
|
+
* @param {string} name - 集合名称
|
|
25
|
+
*/
|
|
26
|
+
constructor(db, name) {
|
|
27
|
+
this.db = db;
|
|
28
|
+
this.name = name;
|
|
29
|
+
this._filePath = join(db.dbPath, `${name}.json`);
|
|
30
|
+
this._data = null;
|
|
31
|
+
this._lock = null; // 简单的锁机制
|
|
32
|
+
this.jsonb = db.options.jsonb; // 继承数据库的 JSONB 选项
|
|
33
|
+
this._schema = null; // Schema 验证
|
|
34
|
+
this._validateOnInsert = true; // 插入时验证
|
|
35
|
+
this._validateOnUpdate = false; // 更新时验证
|
|
36
|
+
|
|
37
|
+
// 内存缓存优化
|
|
38
|
+
this._cache = null;
|
|
39
|
+
this._cacheTime = 0;
|
|
40
|
+
this._cacheTTL = db.options.cacheTTL || 5000; // 默认 5 秒缓存
|
|
41
|
+
this._dirty = false; // 数据是否被修改
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 设置 Schema
|
|
46
|
+
* @param {Schema} schema - Schema 实例
|
|
47
|
+
* @param {Object} options - 选项
|
|
48
|
+
* @returns {Collection}
|
|
49
|
+
*/
|
|
50
|
+
setSchema(schema, options = {}) {
|
|
51
|
+
this._schema = schema;
|
|
52
|
+
this._validateOnInsert = options.validateOnInsert !== false;
|
|
53
|
+
this._validateOnUpdate = options.validateOnUpdate || false;
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 获取 Schema
|
|
59
|
+
* @returns {Schema|null}
|
|
60
|
+
*/
|
|
61
|
+
getSchema() {
|
|
62
|
+
return this._schema;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 验证文档
|
|
67
|
+
* @private
|
|
68
|
+
* @param {Object} doc - 文档
|
|
69
|
+
* @throws {ValidationError}
|
|
70
|
+
*/
|
|
71
|
+
_validate(doc) {
|
|
72
|
+
if (!this._schema) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = this._schema.validate(doc);
|
|
77
|
+
if (!result.valid) {
|
|
78
|
+
throw new ValidationError(`验证失败:${result.errors.map(e => `${e.field}: ${e.message}`).join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 加载集合数据(带缓存)
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
async _load() {
|
|
87
|
+
if (this._data !== null) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 检查内存缓存
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (this._cache && (now - this._cacheTime) < this._cacheTTL) {
|
|
94
|
+
this._data = this._cache;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 等待锁
|
|
99
|
+
await this._acquireLock();
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
if (!existsSync(this._filePath)) {
|
|
103
|
+
// 如果集合文件不存在,自动创建
|
|
104
|
+
this._data = {
|
|
105
|
+
_meta: { name: this.name, count: 0, indexes: [] },
|
|
106
|
+
_documents: [],
|
|
107
|
+
_indexes: {}
|
|
108
|
+
};
|
|
109
|
+
await this._save();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// JSONB 模式:读取二进制文件
|
|
114
|
+
if (this.jsonb) {
|
|
115
|
+
const buffer = await readFile(this._filePath);
|
|
116
|
+
this._data = this._decodeJsonb(buffer);
|
|
117
|
+
} else {
|
|
118
|
+
// 普通模式:读取文本文件
|
|
119
|
+
const content = await readFile(this._filePath, 'utf-8');
|
|
120
|
+
this._data = JSON.parse(content);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 确保数据结构完整
|
|
124
|
+
if (!this._data._meta) {
|
|
125
|
+
this._data._meta = { name: this.name, count: 0, indexes: [] };
|
|
126
|
+
}
|
|
127
|
+
if (!this._data._documents) {
|
|
128
|
+
this._data._documents = [];
|
|
129
|
+
}
|
|
130
|
+
if (!this._data._indexes) {
|
|
131
|
+
this._data._indexes = {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 更新缓存
|
|
135
|
+
this._cache = this._data;
|
|
136
|
+
this._cacheTime = now;
|
|
137
|
+
} finally {
|
|
138
|
+
this._releaseLock();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 保存集合数据(带缓存失效)
|
|
144
|
+
* @private
|
|
145
|
+
*/
|
|
146
|
+
async _save() {
|
|
147
|
+
if (this._data === null) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 标记为脏数据
|
|
152
|
+
this._dirty = true;
|
|
153
|
+
|
|
154
|
+
// 更新元数据
|
|
155
|
+
this._data._meta.count = this._data._documents.length;
|
|
156
|
+
|
|
157
|
+
// 失效缓存
|
|
158
|
+
this._cache = null;
|
|
159
|
+
this._cacheTime = 0;
|
|
160
|
+
|
|
161
|
+
// JSONB 模式:写入二进制文件
|
|
162
|
+
if (this.jsonb) {
|
|
163
|
+
const buffer = this._encodeJsonb();
|
|
164
|
+
await writeFile(this._filePath, buffer);
|
|
165
|
+
} else {
|
|
166
|
+
// 普通模式:写入格式化的 JSON 文本
|
|
167
|
+
const content = JSON.stringify(this._data, null, 2);
|
|
168
|
+
await writeFile(this._filePath, content, 'utf-8');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* JSONB 编码 - 将数据编码为二进制 Buffer
|
|
174
|
+
* 格式:[4 字节长度][JSON 数据的 UTF-8 字节]
|
|
175
|
+
* @private
|
|
176
|
+
* @returns {Buffer} 编码后的 Buffer
|
|
177
|
+
*/
|
|
178
|
+
_encodeJsonb() {
|
|
179
|
+
const json = JSON.stringify(this._data);
|
|
180
|
+
const jsonBuffer = Buffer.from(json, 'utf-8');
|
|
181
|
+
|
|
182
|
+
// 创建包含长度前缀的 Buffer
|
|
183
|
+
// 4 字节 (uint32) 长度 + JSON 数据
|
|
184
|
+
const lengthBuffer = Buffer.alloc(4);
|
|
185
|
+
lengthBuffer.writeUInt32BE(jsonBuffer.length, 0);
|
|
186
|
+
|
|
187
|
+
return Buffer.concat([lengthBuffer, jsonBuffer]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* JSONB 解码 - 将二进制 Buffer 解码为数据
|
|
192
|
+
* @private
|
|
193
|
+
* @param {Buffer} buffer - 编码后的 Buffer
|
|
194
|
+
* @returns {Object} 解码后的数据
|
|
195
|
+
*/
|
|
196
|
+
_decodeJsonb(buffer) {
|
|
197
|
+
try {
|
|
198
|
+
// 读取长度前缀
|
|
199
|
+
const length = buffer.readUInt32BE(0);
|
|
200
|
+
|
|
201
|
+
// 验证长度
|
|
202
|
+
if (length !== buffer.length - 4) {
|
|
203
|
+
throw new Error('JSONB 长度不匹配');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 读取 JSON 数据
|
|
207
|
+
const jsonBuffer = buffer.subarray(4);
|
|
208
|
+
const json = jsonBuffer.toString('utf-8');
|
|
209
|
+
return JSON.parse(json);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
// 如果不是有效的 JSONB 格式,尝试直接 JSON 解析(向后兼容)
|
|
212
|
+
try {
|
|
213
|
+
const json = buffer.toString('utf-8');
|
|
214
|
+
return JSON.parse(json);
|
|
215
|
+
} catch (e2) {
|
|
216
|
+
throw new Error(`无法解析数据:${e.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 获取锁
|
|
223
|
+
* @private
|
|
224
|
+
*/
|
|
225
|
+
async _acquireLock() {
|
|
226
|
+
while (this._lock) {
|
|
227
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
228
|
+
}
|
|
229
|
+
this._lock = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 释放锁
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
_releaseLock() {
|
|
237
|
+
this._lock = null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 获取所有文档
|
|
242
|
+
* @private
|
|
243
|
+
* @returns {Array} 文档数组
|
|
244
|
+
*/
|
|
245
|
+
async _getDocuments() {
|
|
246
|
+
await this._load();
|
|
247
|
+
return this._data._documents;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 插入单个文档
|
|
252
|
+
* @param {Object} doc - 文档
|
|
253
|
+
* @returns {Promise<Object>} 插入后的文档(包含 _id)
|
|
254
|
+
*/
|
|
255
|
+
async insertOne(doc) {
|
|
256
|
+
await this._load();
|
|
257
|
+
|
|
258
|
+
// Schema 验证
|
|
259
|
+
if (this._validateOnInsert) {
|
|
260
|
+
this._validate(doc);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 创建文档副本并添加 _id
|
|
264
|
+
const newDoc = {
|
|
265
|
+
...deepClone(doc),
|
|
266
|
+
_id: doc._id || generateId(),
|
|
267
|
+
createdAt: new Date().toISOString()
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
this._data._documents.push(newDoc);
|
|
271
|
+
await this._save();
|
|
272
|
+
|
|
273
|
+
return deepClone(newDoc);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 插入多个文档(优化版 - 单次写入)
|
|
278
|
+
* @param {Array<Object>} docs - 文档数组
|
|
279
|
+
* @param {Object} options - 选项
|
|
280
|
+
* @returns {Promise<Object>} 插入结果
|
|
281
|
+
*/
|
|
282
|
+
async insertMany(docs, options = {}) {
|
|
283
|
+
if (!Array.isArray(docs)) {
|
|
284
|
+
throw new Error('insertMany 需要数组参数');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await this._load();
|
|
288
|
+
|
|
289
|
+
// 批量验证
|
|
290
|
+
if (this._validateOnInsert) {
|
|
291
|
+
for (const doc of docs) {
|
|
292
|
+
this._validate(doc);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 批量创建文档(带 _id 和 createdAt)
|
|
297
|
+
const now = new Date().toISOString();
|
|
298
|
+
const insertedDocs = docs.map(doc => ({
|
|
299
|
+
...deepClone(doc),
|
|
300
|
+
_id: doc._id || generateId(),
|
|
301
|
+
createdAt: doc.createdAt || now
|
|
302
|
+
}));
|
|
303
|
+
|
|
304
|
+
// 单次批量添加
|
|
305
|
+
this._data._documents.push(...insertedDocs);
|
|
306
|
+
|
|
307
|
+
// 更新索引(如果有)
|
|
308
|
+
if (this._data._meta.indexes?.length > 0) {
|
|
309
|
+
await this._updateIndexes(insertedDocs, 'insert');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 单次保存
|
|
313
|
+
await this._save();
|
|
314
|
+
|
|
315
|
+
// 失效缓存
|
|
316
|
+
this._cache = null;
|
|
317
|
+
this._cacheTime = 0;
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
acknowledged: true,
|
|
321
|
+
insertedCount: insertedDocs.length,
|
|
322
|
+
insertedIds: insertedDocs.reduce((acc, doc, i) => {
|
|
323
|
+
acc[i] = doc._id;
|
|
324
|
+
return acc;
|
|
325
|
+
}, {})
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 查询文档
|
|
331
|
+
* @param {Object} query - 查询条件
|
|
332
|
+
* @param {Object} options - 查询选项
|
|
333
|
+
* @returns {Cursor} 游标对象
|
|
334
|
+
*/
|
|
335
|
+
find(query = {}, options = {}) {
|
|
336
|
+
return new Cursor(this, query, options);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 查询单个文档
|
|
341
|
+
* @param {Object} query - 查询条件
|
|
342
|
+
* @param {Object} options - 查询选项
|
|
343
|
+
* @returns {Promise<Object|null>} 文档或 null
|
|
344
|
+
*/
|
|
345
|
+
async findOne(query = {}, options = {}) {
|
|
346
|
+
return await new Cursor(this, query, options).first();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 更新单个文档
|
|
351
|
+
* @param {Object} query - 查询条件
|
|
352
|
+
* @param {Object} update - 更新操作
|
|
353
|
+
* @param {Object} options - 选项
|
|
354
|
+
* @returns {Promise<Object>} 更新结果
|
|
355
|
+
*/
|
|
356
|
+
async updateOne(query, update, options = {}) {
|
|
357
|
+
await this._load();
|
|
358
|
+
|
|
359
|
+
const docIndex = this._data._documents.findIndex(doc => matchQuery(doc, query));
|
|
360
|
+
|
|
361
|
+
if (docIndex === -1) {
|
|
362
|
+
if (options.upsert) {
|
|
363
|
+
// 插入新文档
|
|
364
|
+
const newDoc = {
|
|
365
|
+
_id: generateId(),
|
|
366
|
+
...deepClone(query),
|
|
367
|
+
...deepClone(update)
|
|
368
|
+
};
|
|
369
|
+
this._data._documents.push(newDoc);
|
|
370
|
+
await this._save();
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
acknowledged: true,
|
|
374
|
+
matchedCount: 0,
|
|
375
|
+
modifiedCount: 0,
|
|
376
|
+
upsertedId: newDoc._id
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
acknowledged: true,
|
|
382
|
+
matchedCount: 0,
|
|
383
|
+
modifiedCount: 0
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 应用更新
|
|
388
|
+
const updatedDoc = applyUpdate(this._data._documents[docIndex], update);
|
|
389
|
+
updatedDoc.updatedAt = new Date().toISOString();
|
|
390
|
+
this._data._documents[docIndex] = updatedDoc;
|
|
391
|
+
|
|
392
|
+
await this._save();
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
acknowledged: true,
|
|
396
|
+
matchedCount: 1,
|
|
397
|
+
modifiedCount: 1
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 更新多个文档
|
|
403
|
+
* @param {Object} query - 查询条件
|
|
404
|
+
* @param {Object} update - 更新操作
|
|
405
|
+
* @param {Object} options - 选项
|
|
406
|
+
* @returns {Promise<Object>} 更新结果
|
|
407
|
+
*/
|
|
408
|
+
async updateMany(query, update, options = {}) {
|
|
409
|
+
await this._load();
|
|
410
|
+
|
|
411
|
+
let matchedCount = 0;
|
|
412
|
+
let modifiedCount = 0;
|
|
413
|
+
|
|
414
|
+
for (let i = 0; i < this._data._documents.length; i++) {
|
|
415
|
+
const doc = this._data._documents[i];
|
|
416
|
+
|
|
417
|
+
if (matchQuery(doc, query)) {
|
|
418
|
+
matchedCount++;
|
|
419
|
+
const updatedDoc = applyUpdate(doc, update);
|
|
420
|
+
updatedDoc.updatedAt = new Date().toISOString();
|
|
421
|
+
|
|
422
|
+
if (JSON.stringify(doc) !== JSON.stringify(updatedDoc)) {
|
|
423
|
+
modifiedCount++;
|
|
424
|
+
this._data._documents[i] = updatedDoc;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (matchedCount === 0 && options.upsert) {
|
|
430
|
+
// 插入新文档
|
|
431
|
+
const newDoc = {
|
|
432
|
+
_id: generateId(),
|
|
433
|
+
...deepClone(query),
|
|
434
|
+
...deepClone(update)
|
|
435
|
+
};
|
|
436
|
+
this._data._documents.push(newDoc);
|
|
437
|
+
await this._save();
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
acknowledged: true,
|
|
441
|
+
matchedCount: 0,
|
|
442
|
+
modifiedCount: 0,
|
|
443
|
+
upsertedId: newDoc._id
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
await this._save();
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
acknowledged: true,
|
|
451
|
+
matchedCount,
|
|
452
|
+
modifiedCount
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* 替换单个文档
|
|
458
|
+
* @param {Object} query - 查询条件
|
|
459
|
+
* @param {Object} doc - 新文档
|
|
460
|
+
* @param {Object} options - 选项
|
|
461
|
+
* @returns {Promise<Object>} 替换结果
|
|
462
|
+
*/
|
|
463
|
+
async replaceOne(query, doc, options = {}) {
|
|
464
|
+
await this._load();
|
|
465
|
+
|
|
466
|
+
const docIndex = this._data._documents.findIndex(doc => matchQuery(doc, query));
|
|
467
|
+
|
|
468
|
+
if (docIndex === -1) {
|
|
469
|
+
if (options.upsert) {
|
|
470
|
+
const newDoc = {
|
|
471
|
+
...deepClone(doc),
|
|
472
|
+
_id: generateId(),
|
|
473
|
+
createdAt: new Date().toISOString()
|
|
474
|
+
};
|
|
475
|
+
this._data._documents.push(newDoc);
|
|
476
|
+
await this._save();
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
acknowledged: true,
|
|
480
|
+
matchedCount: 0,
|
|
481
|
+
modifiedCount: 0,
|
|
482
|
+
upsertedId: newDoc._id
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
acknowledged: true,
|
|
488
|
+
matchedCount: 0,
|
|
489
|
+
modifiedCount: 0
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 保留 _id,替换其他字段
|
|
494
|
+
const oldId = this._data._documents[docIndex]._id;
|
|
495
|
+
const newDoc = {
|
|
496
|
+
...deepClone(doc),
|
|
497
|
+
_id: oldId,
|
|
498
|
+
updatedAt: new Date().toISOString()
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
this._data._documents[docIndex] = newDoc;
|
|
502
|
+
await this._save();
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
acknowledged: true,
|
|
506
|
+
matchedCount: 1,
|
|
507
|
+
modifiedCount: 1
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 删除单个文档
|
|
513
|
+
* @param {Object} query - 查询条件
|
|
514
|
+
* @returns {Promise<Object>} 删除结果
|
|
515
|
+
*/
|
|
516
|
+
async deleteOne(query) {
|
|
517
|
+
await this._load();
|
|
518
|
+
|
|
519
|
+
const docIndex = this._data._documents.findIndex(doc => matchQuery(doc, query));
|
|
520
|
+
|
|
521
|
+
if (docIndex === -1) {
|
|
522
|
+
return {
|
|
523
|
+
acknowledged: true,
|
|
524
|
+
deletedCount: 0
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
this._data._documents.splice(docIndex, 1);
|
|
529
|
+
await this._save();
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
acknowledged: true,
|
|
533
|
+
deletedCount: 1
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* 删除多个文档
|
|
539
|
+
* @param {Object} query - 查询条件
|
|
540
|
+
* @returns {Promise<Object>} 删除结果
|
|
541
|
+
*/
|
|
542
|
+
async deleteMany(query) {
|
|
543
|
+
await this._load();
|
|
544
|
+
|
|
545
|
+
const initialCount = this._data._documents.length;
|
|
546
|
+
this._data._documents = this._data._documents.filter(doc => !matchQuery(doc, query));
|
|
547
|
+
const deletedCount = initialCount - this._data._documents.length;
|
|
548
|
+
|
|
549
|
+
await this._save();
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
acknowledged: true,
|
|
553
|
+
deletedCount
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* 计数文档
|
|
559
|
+
* @param {Object} query - 查询条件
|
|
560
|
+
* @returns {Promise<number>} 文档数量
|
|
561
|
+
*/
|
|
562
|
+
async countDocuments(query = {}) {
|
|
563
|
+
return await this.find(query).count();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* 获取不同值
|
|
568
|
+
* @param {string} key - 字段名
|
|
569
|
+
* @param {Object} query - 查询条件
|
|
570
|
+
* @returns {Promise<Array>} 不同值数组
|
|
571
|
+
*/
|
|
572
|
+
async distinct(key, query = {}) {
|
|
573
|
+
const docs = await this.find(query).toArray();
|
|
574
|
+
const values = new Set();
|
|
575
|
+
|
|
576
|
+
for (const doc of docs) {
|
|
577
|
+
const value = getNestedValue(doc, key);
|
|
578
|
+
if (value !== undefined) {
|
|
579
|
+
values.add(value);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return Array.from(values);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* 聚合查询
|
|
588
|
+
* @param {Array} pipeline - 聚合管道
|
|
589
|
+
* @returns {Promise<Array>} 结果数组
|
|
590
|
+
*/
|
|
591
|
+
async aggregate(pipeline) {
|
|
592
|
+
await this._load();
|
|
593
|
+
|
|
594
|
+
let results = this._data._documents.map(doc => deepClone(doc));
|
|
595
|
+
|
|
596
|
+
for (const stage of pipeline) {
|
|
597
|
+
const stageName = Object.keys(stage)[0];
|
|
598
|
+
const stageValue = stage[stageName];
|
|
599
|
+
|
|
600
|
+
results = this._applyAggregationStage(results, stageName, stageValue);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return results;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* 应用聚合阶段
|
|
608
|
+
* @private
|
|
609
|
+
*/
|
|
610
|
+
_applyAggregationStage(docs, stageName, stageValue) {
|
|
611
|
+
switch (stageName) {
|
|
612
|
+
case '$match':
|
|
613
|
+
return docs.filter(doc => matchQuery(doc, stageValue));
|
|
614
|
+
|
|
615
|
+
case '$project':
|
|
616
|
+
return docs.map(doc => {
|
|
617
|
+
const result = {};
|
|
618
|
+
for (const [key, value] of Object.entries(stageValue)) {
|
|
619
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
620
|
+
result[key] = getNestedValue(doc, value.substring(1));
|
|
621
|
+
} else if (value === 0 || value === false) {
|
|
622
|
+
// 排除字段
|
|
623
|
+
} else {
|
|
624
|
+
result[key] = getNestedValue(doc, key);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (stageValue._id !== undefined) {
|
|
628
|
+
result._id = stageValue._id === 0 ? undefined : doc._id;
|
|
629
|
+
} else {
|
|
630
|
+
result._id = doc._id;
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
case '$group':
|
|
636
|
+
return this._applyGroup(docs, stageValue);
|
|
637
|
+
|
|
638
|
+
case '$sort':
|
|
639
|
+
return docs.sort((a, b) => {
|
|
640
|
+
for (const [key, direction] of Object.entries(stageValue)) {
|
|
641
|
+
const aVal = getNestedValue(a, key);
|
|
642
|
+
const bVal = getNestedValue(b, key);
|
|
643
|
+
if (aVal < bVal) return -1 * direction;
|
|
644
|
+
if (aVal > bVal) return 1 * direction;
|
|
645
|
+
}
|
|
646
|
+
return 0;
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
case '$limit':
|
|
650
|
+
return docs.slice(0, stageValue);
|
|
651
|
+
|
|
652
|
+
case '$skip':
|
|
653
|
+
return docs.slice(stageValue);
|
|
654
|
+
|
|
655
|
+
case '$count':
|
|
656
|
+
return [{ [stageValue]: docs.length }];
|
|
657
|
+
|
|
658
|
+
default:
|
|
659
|
+
console.warn(`未实现的聚合阶段:${stageName}`);
|
|
660
|
+
return docs;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* 应用分组
|
|
666
|
+
* @private
|
|
667
|
+
*/
|
|
668
|
+
_applyGroup(docs, stageValue) {
|
|
669
|
+
const groups = new Map();
|
|
670
|
+
|
|
671
|
+
for (const doc of docs) {
|
|
672
|
+
let key;
|
|
673
|
+
if (stageValue._id === null) {
|
|
674
|
+
key = null;
|
|
675
|
+
} else if (typeof stageValue._id === 'string') {
|
|
676
|
+
key = getNestedValue(doc, stageValue._id.substring(1));
|
|
677
|
+
} else {
|
|
678
|
+
key = JSON.stringify(stageValue._id);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!groups.has(key)) {
|
|
682
|
+
groups.set(key, []);
|
|
683
|
+
}
|
|
684
|
+
groups.get(key).push(doc);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return Array.from(groups.entries()).map(([key, groupDocs]) => {
|
|
688
|
+
const result = { _id: key };
|
|
689
|
+
|
|
690
|
+
for (const [aggKey, aggExpr] of Object.entries(stageValue)) {
|
|
691
|
+
if (aggKey === '_id') continue;
|
|
692
|
+
|
|
693
|
+
if (typeof aggExpr === 'object') {
|
|
694
|
+
const aggOp = Object.keys(aggExpr)[0];
|
|
695
|
+
const aggField = aggExpr[aggOp];
|
|
696
|
+
|
|
697
|
+
if (aggOp === '$sum') {
|
|
698
|
+
result[aggKey] = groupDocs.reduce((sum, d) => {
|
|
699
|
+
const val = getNestedValue(d, aggField.substring(1)) || 0;
|
|
700
|
+
return sum + val;
|
|
701
|
+
}, 0);
|
|
702
|
+
} else if (aggOp === '$avg') {
|
|
703
|
+
result[aggKey] = groupDocs.reduce((sum, d) => {
|
|
704
|
+
const val = getNestedValue(d, aggField.substring(1)) || 0;
|
|
705
|
+
return sum + val;
|
|
706
|
+
}, 0) / groupDocs.length;
|
|
707
|
+
} else if (aggOp === '$count') {
|
|
708
|
+
result[aggKey] = groupDocs.length;
|
|
709
|
+
} else if (aggOp === '$min') {
|
|
710
|
+
result[aggKey] = Math.min(...groupDocs.map(d => getNestedValue(d, aggField.substring(1)) || 0));
|
|
711
|
+
} else if (aggOp === '$max') {
|
|
712
|
+
result[aggKey] = Math.max(...groupDocs.map(d => getNestedValue(d, aggField.substring(1)) || 0));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return result;
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* 创建索引
|
|
723
|
+
* @param {Object} keys - 索引键
|
|
724
|
+
* @param {Object} options - 选项
|
|
725
|
+
* @returns {Promise<Object>} 索引信息
|
|
726
|
+
*/
|
|
727
|
+
async createIndex(keys, options = {}) {
|
|
728
|
+
await this._load();
|
|
729
|
+
|
|
730
|
+
const indexName = Object.entries(keys)
|
|
731
|
+
.map(([k, v]) => `${k}_${v}`)
|
|
732
|
+
.join('_');
|
|
733
|
+
|
|
734
|
+
// 检查索引是否已存在
|
|
735
|
+
const existingIndex = this._data._meta.indexes?.find(i => i.name === indexName);
|
|
736
|
+
if (existingIndex) {
|
|
737
|
+
return existingIndex;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// 创建索引
|
|
741
|
+
const index = {
|
|
742
|
+
key: keys,
|
|
743
|
+
name: indexName,
|
|
744
|
+
unique: options.unique || false
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
if (!this._data._meta.indexes) {
|
|
748
|
+
this._data._meta.indexes = [];
|
|
749
|
+
}
|
|
750
|
+
this._data._meta.indexes.push(index);
|
|
751
|
+
|
|
752
|
+
// 构建索引数据
|
|
753
|
+
await this._buildIndex(index);
|
|
754
|
+
|
|
755
|
+
await this._save();
|
|
756
|
+
|
|
757
|
+
return index;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* 构建索引数据
|
|
762
|
+
* @private
|
|
763
|
+
* @param {Object} index - 索引对象
|
|
764
|
+
*/
|
|
765
|
+
async _buildIndex(index) {
|
|
766
|
+
const indexKeys = Object.keys(index.key);
|
|
767
|
+
const indexData = {};
|
|
768
|
+
|
|
769
|
+
for (const doc of this._data._documents) {
|
|
770
|
+
for (const key of indexKeys) {
|
|
771
|
+
const value = getNestedValue(doc, key);
|
|
772
|
+
if (value !== undefined) {
|
|
773
|
+
const keyStr = String(value);
|
|
774
|
+
if (!indexData[keyStr]) {
|
|
775
|
+
indexData[keyStr] = [];
|
|
776
|
+
}
|
|
777
|
+
indexData[keyStr].push(doc._id);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (!this._data._indexes) {
|
|
783
|
+
this._data._indexes = {};
|
|
784
|
+
}
|
|
785
|
+
this._data._indexes[index.name] = indexData;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* 删除索引
|
|
790
|
+
* @param {string} name - 索引名称
|
|
791
|
+
* @returns {Promise<Object>} 删除结果
|
|
792
|
+
*/
|
|
793
|
+
async dropIndex(name) {
|
|
794
|
+
await this._load();
|
|
795
|
+
|
|
796
|
+
if (!this._data._meta.indexes) {
|
|
797
|
+
return { acknowledged: false };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const initialLength = this._data._meta.indexes.length;
|
|
801
|
+
this._data._meta.indexes = this._data._meta.indexes.filter(i => i.name !== name);
|
|
802
|
+
|
|
803
|
+
if (this._data._meta.indexes.length === initialLength) {
|
|
804
|
+
return { acknowledged: false };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
await this._save();
|
|
808
|
+
|
|
809
|
+
return { acknowledged: true };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* 列出所有索引
|
|
814
|
+
* @returns {Promise<Array>} 索引数组
|
|
815
|
+
*/
|
|
816
|
+
async listIndexes() {
|
|
817
|
+
await this._load();
|
|
818
|
+
return this._data._meta.indexes || [];
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* 获取集合统计信息
|
|
823
|
+
* @returns {Promise<Object>} 统计信息
|
|
824
|
+
*/
|
|
825
|
+
async stats() {
|
|
826
|
+
await this._load();
|
|
827
|
+
|
|
828
|
+
const size = this.jsonb
|
|
829
|
+
? this._encodeJsonb().length
|
|
830
|
+
: JSON.stringify(this._data).length;
|
|
831
|
+
|
|
832
|
+
return {
|
|
833
|
+
ns: this.name,
|
|
834
|
+
count: this._data._documents.length,
|
|
835
|
+
size,
|
|
836
|
+
avgObjSize: this._data._documents.length > 0
|
|
837
|
+
? size / this._data._documents.length
|
|
838
|
+
: 0,
|
|
839
|
+
indexes: this._data._meta.indexes?.length || 0,
|
|
840
|
+
jsonb: this.jsonb
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
}
|