@push.rocks/smartmongo 2.0.12 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/congodb/congodb.plugins.d.ts +10 -0
- package/dist_ts/congodb/congodb.plugins.js +14 -0
- package/dist_ts/congodb/engine/AggregationEngine.d.ts +66 -0
- package/dist_ts/congodb/engine/AggregationEngine.js +189 -0
- package/dist_ts/congodb/engine/IndexEngine.d.ts +77 -0
- package/dist_ts/congodb/engine/IndexEngine.js +376 -0
- package/dist_ts/congodb/engine/QueryEngine.d.ts +54 -0
- package/dist_ts/congodb/engine/QueryEngine.js +271 -0
- package/dist_ts/congodb/engine/TransactionEngine.d.ts +85 -0
- package/dist_ts/congodb/engine/TransactionEngine.js +287 -0
- package/dist_ts/congodb/engine/UpdateEngine.d.ts +47 -0
- package/dist_ts/congodb/engine/UpdateEngine.js +461 -0
- package/dist_ts/congodb/errors/CongoErrors.d.ts +100 -0
- package/dist_ts/congodb/errors/CongoErrors.js +155 -0
- package/dist_ts/congodb/index.d.ts +19 -0
- package/dist_ts/congodb/index.js +26 -0
- package/dist_ts/congodb/server/CommandRouter.d.ts +51 -0
- package/dist_ts/congodb/server/CommandRouter.js +132 -0
- package/dist_ts/congodb/server/CongoServer.d.ts +95 -0
- package/dist_ts/congodb/server/CongoServer.js +227 -0
- package/dist_ts/congodb/server/WireProtocol.d.ts +117 -0
- package/dist_ts/congodb/server/WireProtocol.js +298 -0
- package/dist_ts/congodb/server/handlers/AdminHandler.d.ts +100 -0
- package/dist_ts/congodb/server/handlers/AdminHandler.js +568 -0
- package/dist_ts/congodb/server/handlers/AggregateHandler.d.ts +31 -0
- package/dist_ts/congodb/server/handlers/AggregateHandler.js +277 -0
- package/dist_ts/congodb/server/handlers/DeleteHandler.d.ts +8 -0
- package/dist_ts/congodb/server/handlers/DeleteHandler.js +83 -0
- package/dist_ts/congodb/server/handlers/FindHandler.d.ts +31 -0
- package/dist_ts/congodb/server/handlers/FindHandler.js +261 -0
- package/dist_ts/congodb/server/handlers/HelloHandler.d.ts +11 -0
- package/dist_ts/congodb/server/handlers/HelloHandler.js +62 -0
- package/dist_ts/congodb/server/handlers/IndexHandler.d.ts +20 -0
- package/dist_ts/congodb/server/handlers/IndexHandler.js +183 -0
- package/dist_ts/congodb/server/handlers/InsertHandler.d.ts +8 -0
- package/dist_ts/congodb/server/handlers/InsertHandler.js +76 -0
- package/dist_ts/congodb/server/handlers/UpdateHandler.d.ts +24 -0
- package/dist_ts/congodb/server/handlers/UpdateHandler.js +270 -0
- package/dist_ts/congodb/server/handlers/index.d.ts +8 -0
- package/dist_ts/congodb/server/handlers/index.js +10 -0
- package/dist_ts/congodb/server/index.d.ts +6 -0
- package/dist_ts/congodb/server/index.js +7 -0
- package/dist_ts/congodb/storage/FileStorageAdapter.d.ts +61 -0
- package/dist_ts/congodb/storage/FileStorageAdapter.js +396 -0
- package/dist_ts/congodb/storage/IStorageAdapter.d.ts +140 -0
- package/dist_ts/congodb/storage/IStorageAdapter.js +2 -0
- package/dist_ts/congodb/storage/MemoryStorageAdapter.d.ts +66 -0
- package/dist_ts/congodb/storage/MemoryStorageAdapter.js +367 -0
- package/dist_ts/congodb/storage/OpLog.d.ts +93 -0
- package/dist_ts/congodb/storage/OpLog.js +221 -0
- package/dist_ts/congodb/types/interfaces.d.ts +363 -0
- package/dist_ts/congodb/types/interfaces.js +2 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +8 -6
- package/dist_ts/smartmongo.plugins.d.ts +1 -1
- package/dist_ts/smartmongo.plugins.js +2 -2
- package/npmextra.json +17 -7
- package/package.json +20 -12
- package/readme.hints.md +89 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/congodb/congodb.plugins.ts +17 -0
- package/ts/congodb/engine/AggregationEngine.ts +283 -0
- package/ts/congodb/engine/IndexEngine.ts +479 -0
- package/ts/congodb/engine/QueryEngine.ts +301 -0
- package/ts/congodb/engine/TransactionEngine.ts +351 -0
- package/ts/congodb/engine/UpdateEngine.ts +506 -0
- package/ts/congodb/errors/CongoErrors.ts +181 -0
- package/ts/congodb/index.ts +37 -0
- package/ts/congodb/server/CommandRouter.ts +180 -0
- package/ts/congodb/server/CongoServer.ts +298 -0
- package/ts/congodb/server/WireProtocol.ts +416 -0
- package/ts/congodb/server/handlers/AdminHandler.ts +614 -0
- package/ts/congodb/server/handlers/AggregateHandler.ts +342 -0
- package/ts/congodb/server/handlers/DeleteHandler.ts +100 -0
- package/ts/congodb/server/handlers/FindHandler.ts +301 -0
- package/ts/congodb/server/handlers/HelloHandler.ts +78 -0
- package/ts/congodb/server/handlers/IndexHandler.ts +207 -0
- package/ts/congodb/server/handlers/InsertHandler.ts +91 -0
- package/ts/congodb/server/handlers/UpdateHandler.ts +315 -0
- package/ts/congodb/server/handlers/index.ts +10 -0
- package/ts/congodb/server/index.ts +10 -0
- package/ts/congodb/storage/FileStorageAdapter.ts +479 -0
- package/ts/congodb/storage/IStorageAdapter.ts +202 -0
- package/ts/congodb/storage/MemoryStorageAdapter.ts +443 -0
- package/ts/congodb/storage/OpLog.ts +282 -0
- package/ts/congodb/types/interfaces.ts +433 -0
- package/ts/index.ts +3 -0
- package/ts/smartmongo.plugins.ts +1 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import * as plugins from '../congodb.plugins.js';
|
|
2
|
+
import type { IStorageAdapter } from './IStorageAdapter.js';
|
|
3
|
+
import type { IStoredDocument, IOpLogEntry, Document } from '../types/interfaces.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* File-based storage adapter for CongoDB
|
|
7
|
+
* Stores data in JSON files on disk for persistence
|
|
8
|
+
*/
|
|
9
|
+
export class FileStorageAdapter implements IStorageAdapter {
|
|
10
|
+
private basePath: string;
|
|
11
|
+
private opLogCounter = 0;
|
|
12
|
+
private initialized = false;
|
|
13
|
+
private fs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
|
14
|
+
|
|
15
|
+
constructor(basePath: string) {
|
|
16
|
+
this.basePath = basePath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Helper Methods
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
private getDbPath(dbName: string): string {
|
|
24
|
+
return plugins.smartpath.join(this.basePath, dbName);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private getCollectionPath(dbName: string, collName: string): string {
|
|
28
|
+
return plugins.smartpath.join(this.basePath, dbName, `${collName}.json`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private getIndexPath(dbName: string, collName: string): string {
|
|
32
|
+
return plugins.smartpath.join(this.basePath, dbName, `${collName}.indexes.json`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private getOpLogPath(): string {
|
|
36
|
+
return plugins.smartpath.join(this.basePath, '_oplog.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private getMetaPath(): string {
|
|
40
|
+
return plugins.smartpath.join(this.basePath, '_meta.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async readJsonFile<T>(filePath: string, defaultValue: T): Promise<T> {
|
|
44
|
+
try {
|
|
45
|
+
const exists = await this.fs.file(filePath).exists();
|
|
46
|
+
if (!exists) return defaultValue;
|
|
47
|
+
const content = await this.fs.file(filePath).encoding('utf8').read();
|
|
48
|
+
return JSON.parse(content as string);
|
|
49
|
+
} catch {
|
|
50
|
+
return defaultValue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async writeJsonFile(filePath: string, data: any): Promise<void> {
|
|
55
|
+
const dir = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
56
|
+
await this.fs.directory(dir).recursive().create();
|
|
57
|
+
await this.fs.file(filePath).encoding('utf8').write(JSON.stringify(data, null, 2));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private restoreObjectIds(doc: any): IStoredDocument {
|
|
61
|
+
if (doc._id) {
|
|
62
|
+
if (typeof doc._id === 'string') {
|
|
63
|
+
doc._id = new plugins.bson.ObjectId(doc._id);
|
|
64
|
+
} else if (typeof doc._id === 'object' && doc._id.$oid) {
|
|
65
|
+
doc._id = new plugins.bson.ObjectId(doc._id.$oid);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return doc;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Initialization
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
async initialize(): Promise<void> {
|
|
76
|
+
if (this.initialized) return;
|
|
77
|
+
|
|
78
|
+
await this.fs.directory(this.basePath).recursive().create();
|
|
79
|
+
|
|
80
|
+
// Load metadata
|
|
81
|
+
const meta = await this.readJsonFile(this.getMetaPath(), { opLogCounter: 0 });
|
|
82
|
+
this.opLogCounter = meta.opLogCounter || 0;
|
|
83
|
+
|
|
84
|
+
this.initialized = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async close(): Promise<void> {
|
|
88
|
+
// Save metadata
|
|
89
|
+
await this.writeJsonFile(this.getMetaPath(), { opLogCounter: this.opLogCounter });
|
|
90
|
+
this.initialized = false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Database Operations
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
async listDatabases(): Promise<string[]> {
|
|
98
|
+
await this.initialize();
|
|
99
|
+
try {
|
|
100
|
+
const entries = await this.fs.directory(this.basePath).list();
|
|
101
|
+
return entries
|
|
102
|
+
.filter(entry => entry.isDirectory && !entry.name.startsWith('_'))
|
|
103
|
+
.map(entry => entry.name);
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async createDatabase(dbName: string): Promise<void> {
|
|
110
|
+
await this.initialize();
|
|
111
|
+
const dbPath = this.getDbPath(dbName);
|
|
112
|
+
await this.fs.directory(dbPath).recursive().create();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async dropDatabase(dbName: string): Promise<boolean> {
|
|
116
|
+
await this.initialize();
|
|
117
|
+
const dbPath = this.getDbPath(dbName);
|
|
118
|
+
try {
|
|
119
|
+
const exists = await this.fs.directory(dbPath).exists();
|
|
120
|
+
if (exists) {
|
|
121
|
+
await this.fs.directory(dbPath).recursive().delete();
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async databaseExists(dbName: string): Promise<boolean> {
|
|
131
|
+
await this.initialize();
|
|
132
|
+
const dbPath = this.getDbPath(dbName);
|
|
133
|
+
return this.fs.directory(dbPath).exists();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Collection Operations
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
async listCollections(dbName: string): Promise<string[]> {
|
|
141
|
+
await this.initialize();
|
|
142
|
+
const dbPath = this.getDbPath(dbName);
|
|
143
|
+
try {
|
|
144
|
+
const entries = await this.fs.directory(dbPath).list();
|
|
145
|
+
return entries
|
|
146
|
+
.filter(entry => entry.isFile && entry.name.endsWith('.json') && !entry.name.endsWith('.indexes.json'))
|
|
147
|
+
.map(entry => entry.name.replace('.json', ''));
|
|
148
|
+
} catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async createCollection(dbName: string, collName: string): Promise<void> {
|
|
154
|
+
await this.createDatabase(dbName);
|
|
155
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
156
|
+
const exists = await this.fs.file(collPath).exists();
|
|
157
|
+
if (!exists) {
|
|
158
|
+
await this.writeJsonFile(collPath, []);
|
|
159
|
+
// Create default _id index
|
|
160
|
+
await this.writeJsonFile(this.getIndexPath(dbName, collName), [
|
|
161
|
+
{ name: '_id_', key: { _id: 1 }, unique: true }
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async dropCollection(dbName: string, collName: string): Promise<boolean> {
|
|
167
|
+
await this.initialize();
|
|
168
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
169
|
+
const indexPath = this.getIndexPath(dbName, collName);
|
|
170
|
+
try {
|
|
171
|
+
const exists = await this.fs.file(collPath).exists();
|
|
172
|
+
if (exists) {
|
|
173
|
+
await this.fs.file(collPath).delete();
|
|
174
|
+
try {
|
|
175
|
+
await this.fs.file(indexPath).delete();
|
|
176
|
+
} catch {}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async collectionExists(dbName: string, collName: string): Promise<boolean> {
|
|
186
|
+
await this.initialize();
|
|
187
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
188
|
+
return this.fs.file(collPath).exists();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async renameCollection(dbName: string, oldName: string, newName: string): Promise<void> {
|
|
192
|
+
await this.initialize();
|
|
193
|
+
const oldPath = this.getCollectionPath(dbName, oldName);
|
|
194
|
+
const newPath = this.getCollectionPath(dbName, newName);
|
|
195
|
+
const oldIndexPath = this.getIndexPath(dbName, oldName);
|
|
196
|
+
const newIndexPath = this.getIndexPath(dbName, newName);
|
|
197
|
+
|
|
198
|
+
const exists = await this.fs.file(oldPath).exists();
|
|
199
|
+
if (!exists) {
|
|
200
|
+
throw new Error(`Collection ${oldName} not found`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Read, write to new, delete old
|
|
204
|
+
const docs = await this.readJsonFile<any[]>(oldPath, []);
|
|
205
|
+
await this.writeJsonFile(newPath, docs);
|
|
206
|
+
await this.fs.file(oldPath).delete();
|
|
207
|
+
|
|
208
|
+
// Handle indexes
|
|
209
|
+
const indexes = await this.readJsonFile<any[]>(oldIndexPath, []);
|
|
210
|
+
await this.writeJsonFile(newIndexPath, indexes);
|
|
211
|
+
try {
|
|
212
|
+
await this.fs.file(oldIndexPath).delete();
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Document Operations
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
async insertOne(dbName: string, collName: string, doc: Document): Promise<IStoredDocument> {
|
|
221
|
+
await this.createCollection(dbName, collName);
|
|
222
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
223
|
+
const docs = await this.readJsonFile<any[]>(collPath, []);
|
|
224
|
+
|
|
225
|
+
const storedDoc: IStoredDocument = {
|
|
226
|
+
...doc,
|
|
227
|
+
_id: doc._id ? (doc._id instanceof plugins.bson.ObjectId ? doc._id : new plugins.bson.ObjectId(doc._id)) : new plugins.bson.ObjectId(),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Check for duplicate
|
|
231
|
+
const idStr = storedDoc._id.toHexString();
|
|
232
|
+
if (docs.some(d => d._id === idStr || (d._id && d._id.toString() === idStr))) {
|
|
233
|
+
throw new Error(`Duplicate key error: _id ${idStr}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
docs.push(storedDoc);
|
|
237
|
+
await this.writeJsonFile(collPath, docs);
|
|
238
|
+
return storedDoc;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async insertMany(dbName: string, collName: string, docsToInsert: Document[]): Promise<IStoredDocument[]> {
|
|
242
|
+
await this.createCollection(dbName, collName);
|
|
243
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
244
|
+
const docs = await this.readJsonFile<any[]>(collPath, []);
|
|
245
|
+
|
|
246
|
+
const results: IStoredDocument[] = [];
|
|
247
|
+
const existingIds = new Set(docs.map(d => d._id?.toString?.() || d._id));
|
|
248
|
+
|
|
249
|
+
for (const doc of docsToInsert) {
|
|
250
|
+
const storedDoc: IStoredDocument = {
|
|
251
|
+
...doc,
|
|
252
|
+
_id: doc._id ? (doc._id instanceof plugins.bson.ObjectId ? doc._id : new plugins.bson.ObjectId(doc._id)) : new plugins.bson.ObjectId(),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const idStr = storedDoc._id.toHexString();
|
|
256
|
+
if (existingIds.has(idStr)) {
|
|
257
|
+
throw new Error(`Duplicate key error: _id ${idStr}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
existingIds.add(idStr);
|
|
261
|
+
docs.push(storedDoc);
|
|
262
|
+
results.push(storedDoc);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await this.writeJsonFile(collPath, docs);
|
|
266
|
+
return results;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async findAll(dbName: string, collName: string): Promise<IStoredDocument[]> {
|
|
270
|
+
await this.createCollection(dbName, collName);
|
|
271
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
272
|
+
const docs = await this.readJsonFile<any[]>(collPath, []);
|
|
273
|
+
return docs.map(doc => this.restoreObjectIds(doc));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async findById(dbName: string, collName: string, id: plugins.bson.ObjectId): Promise<IStoredDocument | null> {
|
|
277
|
+
const docs = await this.findAll(dbName, collName);
|
|
278
|
+
const idStr = id.toHexString();
|
|
279
|
+
return docs.find(d => d._id.toHexString() === idStr) || null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async updateById(dbName: string, collName: string, id: plugins.bson.ObjectId, doc: IStoredDocument): Promise<boolean> {
|
|
283
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
284
|
+
const docs = await this.readJsonFile<any[]>(collPath, []);
|
|
285
|
+
const idStr = id.toHexString();
|
|
286
|
+
|
|
287
|
+
const idx = docs.findIndex(d => {
|
|
288
|
+
const docId = d._id?.toHexString?.() || d._id?.toString?.() || d._id;
|
|
289
|
+
return docId === idStr;
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (idx === -1) return false;
|
|
293
|
+
|
|
294
|
+
docs[idx] = doc;
|
|
295
|
+
await this.writeJsonFile(collPath, docs);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async deleteById(dbName: string, collName: string, id: plugins.bson.ObjectId): Promise<boolean> {
|
|
300
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
301
|
+
const docs = await this.readJsonFile<any[]>(collPath, []);
|
|
302
|
+
const idStr = id.toHexString();
|
|
303
|
+
|
|
304
|
+
const idx = docs.findIndex(d => {
|
|
305
|
+
const docId = d._id?.toHexString?.() || d._id?.toString?.() || d._id;
|
|
306
|
+
return docId === idStr;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (idx === -1) return false;
|
|
310
|
+
|
|
311
|
+
docs.splice(idx, 1);
|
|
312
|
+
await this.writeJsonFile(collPath, docs);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async deleteByIds(dbName: string, collName: string, ids: plugins.bson.ObjectId[]): Promise<number> {
|
|
317
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
318
|
+
const docs = await this.readJsonFile<any[]>(collPath, []);
|
|
319
|
+
const idStrs = new Set(ids.map(id => id.toHexString()));
|
|
320
|
+
|
|
321
|
+
const originalLength = docs.length;
|
|
322
|
+
const filtered = docs.filter(d => {
|
|
323
|
+
const docId = d._id?.toHexString?.() || d._id?.toString?.() || d._id;
|
|
324
|
+
return !idStrs.has(docId);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await this.writeJsonFile(collPath, filtered);
|
|
328
|
+
return originalLength - filtered.length;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async count(dbName: string, collName: string): Promise<number> {
|
|
332
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
333
|
+
const docs = await this.readJsonFile<any[]>(collPath, []);
|
|
334
|
+
return docs.length;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// Index Operations
|
|
339
|
+
// ============================================================================
|
|
340
|
+
|
|
341
|
+
async saveIndex(
|
|
342
|
+
dbName: string,
|
|
343
|
+
collName: string,
|
|
344
|
+
indexName: string,
|
|
345
|
+
indexSpec: { key: Record<string, any>; unique?: boolean; sparse?: boolean; expireAfterSeconds?: number }
|
|
346
|
+
): Promise<void> {
|
|
347
|
+
await this.createCollection(dbName, collName);
|
|
348
|
+
const indexPath = this.getIndexPath(dbName, collName);
|
|
349
|
+
const indexes = await this.readJsonFile<any[]>(indexPath, [
|
|
350
|
+
{ name: '_id_', key: { _id: 1 }, unique: true }
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
const existingIdx = indexes.findIndex(i => i.name === indexName);
|
|
354
|
+
if (existingIdx >= 0) {
|
|
355
|
+
indexes[existingIdx] = { name: indexName, ...indexSpec };
|
|
356
|
+
} else {
|
|
357
|
+
indexes.push({ name: indexName, ...indexSpec });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
await this.writeJsonFile(indexPath, indexes);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async getIndexes(dbName: string, collName: string): Promise<Array<{
|
|
364
|
+
name: string;
|
|
365
|
+
key: Record<string, any>;
|
|
366
|
+
unique?: boolean;
|
|
367
|
+
sparse?: boolean;
|
|
368
|
+
expireAfterSeconds?: number;
|
|
369
|
+
}>> {
|
|
370
|
+
const indexPath = this.getIndexPath(dbName, collName);
|
|
371
|
+
return this.readJsonFile(indexPath, [{ name: '_id_', key: { _id: 1 }, unique: true }]);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async dropIndex(dbName: string, collName: string, indexName: string): Promise<boolean> {
|
|
375
|
+
if (indexName === '_id_') {
|
|
376
|
+
throw new Error('Cannot drop _id index');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const indexPath = this.getIndexPath(dbName, collName);
|
|
380
|
+
const indexes = await this.readJsonFile<any[]>(indexPath, []);
|
|
381
|
+
|
|
382
|
+
const idx = indexes.findIndex(i => i.name === indexName);
|
|
383
|
+
if (idx >= 0) {
|
|
384
|
+
indexes.splice(idx, 1);
|
|
385
|
+
await this.writeJsonFile(indexPath, indexes);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// OpLog Operations
|
|
393
|
+
// ============================================================================
|
|
394
|
+
|
|
395
|
+
async appendOpLog(entry: IOpLogEntry): Promise<void> {
|
|
396
|
+
const opLogPath = this.getOpLogPath();
|
|
397
|
+
const opLog = await this.readJsonFile<IOpLogEntry[]>(opLogPath, []);
|
|
398
|
+
opLog.push(entry);
|
|
399
|
+
|
|
400
|
+
// Trim oplog if it gets too large
|
|
401
|
+
if (opLog.length > 10000) {
|
|
402
|
+
opLog.splice(0, opLog.length - 10000);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
await this.writeJsonFile(opLogPath, opLog);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async getOpLogAfter(ts: plugins.bson.Timestamp, limit: number = 1000): Promise<IOpLogEntry[]> {
|
|
409
|
+
const opLogPath = this.getOpLogPath();
|
|
410
|
+
const opLog = await this.readJsonFile<any[]>(opLogPath, []);
|
|
411
|
+
const tsValue = ts.toNumber();
|
|
412
|
+
|
|
413
|
+
const entries = opLog.filter(e => {
|
|
414
|
+
const entryTs = e.ts.toNumber ? e.ts.toNumber() : (e.ts.t * 4294967296 + e.ts.i);
|
|
415
|
+
return entryTs > tsValue;
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return entries.slice(0, limit);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async getLatestOpLogTimestamp(): Promise<plugins.bson.Timestamp | null> {
|
|
422
|
+
const opLogPath = this.getOpLogPath();
|
|
423
|
+
const opLog = await this.readJsonFile<any[]>(opLogPath, []);
|
|
424
|
+
if (opLog.length === 0) return null;
|
|
425
|
+
|
|
426
|
+
const last = opLog[opLog.length - 1];
|
|
427
|
+
if (last.ts instanceof plugins.bson.Timestamp) {
|
|
428
|
+
return last.ts;
|
|
429
|
+
}
|
|
430
|
+
return new plugins.bson.Timestamp({ t: last.ts.t, i: last.ts.i });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
generateTimestamp(): plugins.bson.Timestamp {
|
|
434
|
+
this.opLogCounter++;
|
|
435
|
+
return new plugins.bson.Timestamp({ t: Math.floor(Date.now() / 1000), i: this.opLogCounter });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// Transaction Support
|
|
440
|
+
// ============================================================================
|
|
441
|
+
|
|
442
|
+
async createSnapshot(dbName: string, collName: string): Promise<IStoredDocument[]> {
|
|
443
|
+
const docs = await this.findAll(dbName, collName);
|
|
444
|
+
return docs.map(doc => JSON.parse(JSON.stringify(doc)));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async hasConflicts(
|
|
448
|
+
dbName: string,
|
|
449
|
+
collName: string,
|
|
450
|
+
ids: plugins.bson.ObjectId[],
|
|
451
|
+
snapshotTime: plugins.bson.Timestamp
|
|
452
|
+
): Promise<boolean> {
|
|
453
|
+
const opLogPath = this.getOpLogPath();
|
|
454
|
+
const opLog = await this.readJsonFile<any[]>(opLogPath, []);
|
|
455
|
+
const ns = `${dbName}.${collName}`;
|
|
456
|
+
const snapshotTs = snapshotTime.toNumber();
|
|
457
|
+
const modifiedIds = new Set<string>();
|
|
458
|
+
|
|
459
|
+
for (const entry of opLog) {
|
|
460
|
+
const entryTs = entry.ts.toNumber ? entry.ts.toNumber() : (entry.ts.t * 4294967296 + entry.ts.i);
|
|
461
|
+
if (entryTs > snapshotTs && entry.ns === ns) {
|
|
462
|
+
if (entry.o._id) {
|
|
463
|
+
modifiedIds.add(entry.o._id.toString());
|
|
464
|
+
}
|
|
465
|
+
if (entry.o2?._id) {
|
|
466
|
+
modifiedIds.add(entry.o2._id.toString());
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (const id of ids) {
|
|
472
|
+
if (modifiedIds.has(id.toString())) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type * as plugins from '../congodb.plugins.js';
|
|
2
|
+
import type { IStoredDocument, IOpLogEntry, Document } from '../types/interfaces.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Storage adapter interface for CongoDB
|
|
6
|
+
* Implementations can provide different storage backends (memory, file, etc.)
|
|
7
|
+
*/
|
|
8
|
+
export interface IStorageAdapter {
|
|
9
|
+
/**
|
|
10
|
+
* Initialize the storage adapter
|
|
11
|
+
*/
|
|
12
|
+
initialize(): Promise<void>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Close the storage adapter and release resources
|
|
16
|
+
*/
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Database Operations
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* List all database names
|
|
25
|
+
*/
|
|
26
|
+
listDatabases(): Promise<string[]>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a database (typically lazy - just marks it as existing)
|
|
30
|
+
*/
|
|
31
|
+
createDatabase(dbName: string): Promise<void>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Drop a database and all its collections
|
|
35
|
+
*/
|
|
36
|
+
dropDatabase(dbName: string): Promise<boolean>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a database exists
|
|
40
|
+
*/
|
|
41
|
+
databaseExists(dbName: string): Promise<boolean>;
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Collection Operations
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* List all collection names in a database
|
|
49
|
+
*/
|
|
50
|
+
listCollections(dbName: string): Promise<string[]>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a collection
|
|
54
|
+
*/
|
|
55
|
+
createCollection(dbName: string, collName: string): Promise<void>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Drop a collection
|
|
59
|
+
*/
|
|
60
|
+
dropCollection(dbName: string, collName: string): Promise<boolean>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a collection exists
|
|
64
|
+
*/
|
|
65
|
+
collectionExists(dbName: string, collName: string): Promise<boolean>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Rename a collection
|
|
69
|
+
*/
|
|
70
|
+
renameCollection(dbName: string, oldName: string, newName: string): Promise<void>;
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Document Operations
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Insert a single document
|
|
78
|
+
* @returns The inserted document with _id
|
|
79
|
+
*/
|
|
80
|
+
insertOne(dbName: string, collName: string, doc: Document): Promise<IStoredDocument>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Insert multiple documents
|
|
84
|
+
* @returns Array of inserted documents with _ids
|
|
85
|
+
*/
|
|
86
|
+
insertMany(dbName: string, collName: string, docs: Document[]): Promise<IStoredDocument[]>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find all documents in a collection
|
|
90
|
+
*/
|
|
91
|
+
findAll(dbName: string, collName: string): Promise<IStoredDocument[]>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Find a document by _id
|
|
95
|
+
*/
|
|
96
|
+
findById(dbName: string, collName: string, id: plugins.bson.ObjectId): Promise<IStoredDocument | null>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Update a document by _id
|
|
100
|
+
* @returns true if document was updated
|
|
101
|
+
*/
|
|
102
|
+
updateById(dbName: string, collName: string, id: plugins.bson.ObjectId, doc: IStoredDocument): Promise<boolean>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Delete a document by _id
|
|
106
|
+
* @returns true if document was deleted
|
|
107
|
+
*/
|
|
108
|
+
deleteById(dbName: string, collName: string, id: plugins.bson.ObjectId): Promise<boolean>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Delete multiple documents by _id
|
|
112
|
+
* @returns Number of deleted documents
|
|
113
|
+
*/
|
|
114
|
+
deleteByIds(dbName: string, collName: string, ids: plugins.bson.ObjectId[]): Promise<number>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the count of documents in a collection
|
|
118
|
+
*/
|
|
119
|
+
count(dbName: string, collName: string): Promise<number>;
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// Index Operations
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Store index metadata
|
|
127
|
+
*/
|
|
128
|
+
saveIndex(
|
|
129
|
+
dbName: string,
|
|
130
|
+
collName: string,
|
|
131
|
+
indexName: string,
|
|
132
|
+
indexSpec: { key: Record<string, any>; unique?: boolean; sparse?: boolean; expireAfterSeconds?: number }
|
|
133
|
+
): Promise<void>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get all index metadata for a collection
|
|
137
|
+
*/
|
|
138
|
+
getIndexes(dbName: string, collName: string): Promise<Array<{
|
|
139
|
+
name: string;
|
|
140
|
+
key: Record<string, any>;
|
|
141
|
+
unique?: boolean;
|
|
142
|
+
sparse?: boolean;
|
|
143
|
+
expireAfterSeconds?: number;
|
|
144
|
+
}>>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Drop an index
|
|
148
|
+
*/
|
|
149
|
+
dropIndex(dbName: string, collName: string, indexName: string): Promise<boolean>;
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// OpLog Operations (for change streams)
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Append an operation to the oplog
|
|
157
|
+
*/
|
|
158
|
+
appendOpLog(entry: IOpLogEntry): Promise<void>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get oplog entries after a timestamp
|
|
162
|
+
*/
|
|
163
|
+
getOpLogAfter(ts: plugins.bson.Timestamp, limit?: number): Promise<IOpLogEntry[]>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get the latest oplog timestamp
|
|
167
|
+
*/
|
|
168
|
+
getLatestOpLogTimestamp(): Promise<plugins.bson.Timestamp | null>;
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Transaction Support
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create a snapshot of current data for transaction isolation
|
|
176
|
+
*/
|
|
177
|
+
createSnapshot(dbName: string, collName: string): Promise<IStoredDocument[]>;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if any documents have been modified since the snapshot
|
|
181
|
+
*/
|
|
182
|
+
hasConflicts(
|
|
183
|
+
dbName: string,
|
|
184
|
+
collName: string,
|
|
185
|
+
ids: plugins.bson.ObjectId[],
|
|
186
|
+
snapshotTime: plugins.bson.Timestamp
|
|
187
|
+
): Promise<boolean>;
|
|
188
|
+
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// Persistence (optional, for MemoryStorageAdapter with file backup)
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Persist current state to disk (if supported)
|
|
195
|
+
*/
|
|
196
|
+
persist?(): Promise<void>;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Load state from disk (if supported)
|
|
200
|
+
*/
|
|
201
|
+
restore?(): Promise<void>;
|
|
202
|
+
}
|