@push.rocks/smartmongo 2.0.14 → 2.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.
Files changed (87) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/congodb/congodb.plugins.d.ts +10 -0
  3. package/dist_ts/congodb/congodb.plugins.js +14 -0
  4. package/dist_ts/congodb/engine/AggregationEngine.d.ts +66 -0
  5. package/dist_ts/congodb/engine/AggregationEngine.js +189 -0
  6. package/dist_ts/congodb/engine/IndexEngine.d.ts +77 -0
  7. package/dist_ts/congodb/engine/IndexEngine.js +376 -0
  8. package/dist_ts/congodb/engine/QueryEngine.d.ts +54 -0
  9. package/dist_ts/congodb/engine/QueryEngine.js +271 -0
  10. package/dist_ts/congodb/engine/TransactionEngine.d.ts +85 -0
  11. package/dist_ts/congodb/engine/TransactionEngine.js +287 -0
  12. package/dist_ts/congodb/engine/UpdateEngine.d.ts +47 -0
  13. package/dist_ts/congodb/engine/UpdateEngine.js +461 -0
  14. package/dist_ts/congodb/errors/CongoErrors.d.ts +100 -0
  15. package/dist_ts/congodb/errors/CongoErrors.js +155 -0
  16. package/dist_ts/congodb/index.d.ts +19 -0
  17. package/dist_ts/congodb/index.js +26 -0
  18. package/dist_ts/congodb/server/CommandRouter.d.ts +51 -0
  19. package/dist_ts/congodb/server/CommandRouter.js +132 -0
  20. package/dist_ts/congodb/server/CongoServer.d.ts +95 -0
  21. package/dist_ts/congodb/server/CongoServer.js +227 -0
  22. package/dist_ts/congodb/server/WireProtocol.d.ts +117 -0
  23. package/dist_ts/congodb/server/WireProtocol.js +298 -0
  24. package/dist_ts/congodb/server/handlers/AdminHandler.d.ts +100 -0
  25. package/dist_ts/congodb/server/handlers/AdminHandler.js +568 -0
  26. package/dist_ts/congodb/server/handlers/AggregateHandler.d.ts +31 -0
  27. package/dist_ts/congodb/server/handlers/AggregateHandler.js +277 -0
  28. package/dist_ts/congodb/server/handlers/DeleteHandler.d.ts +8 -0
  29. package/dist_ts/congodb/server/handlers/DeleteHandler.js +83 -0
  30. package/dist_ts/congodb/server/handlers/FindHandler.d.ts +31 -0
  31. package/dist_ts/congodb/server/handlers/FindHandler.js +261 -0
  32. package/dist_ts/congodb/server/handlers/HelloHandler.d.ts +11 -0
  33. package/dist_ts/congodb/server/handlers/HelloHandler.js +62 -0
  34. package/dist_ts/congodb/server/handlers/IndexHandler.d.ts +20 -0
  35. package/dist_ts/congodb/server/handlers/IndexHandler.js +183 -0
  36. package/dist_ts/congodb/server/handlers/InsertHandler.d.ts +8 -0
  37. package/dist_ts/congodb/server/handlers/InsertHandler.js +76 -0
  38. package/dist_ts/congodb/server/handlers/UpdateHandler.d.ts +24 -0
  39. package/dist_ts/congodb/server/handlers/UpdateHandler.js +270 -0
  40. package/dist_ts/congodb/server/handlers/index.d.ts +8 -0
  41. package/dist_ts/congodb/server/handlers/index.js +10 -0
  42. package/dist_ts/congodb/server/index.d.ts +6 -0
  43. package/dist_ts/congodb/server/index.js +7 -0
  44. package/dist_ts/congodb/storage/FileStorageAdapter.d.ts +61 -0
  45. package/dist_ts/congodb/storage/FileStorageAdapter.js +396 -0
  46. package/dist_ts/congodb/storage/IStorageAdapter.d.ts +140 -0
  47. package/dist_ts/congodb/storage/IStorageAdapter.js +2 -0
  48. package/dist_ts/congodb/storage/MemoryStorageAdapter.d.ts +66 -0
  49. package/dist_ts/congodb/storage/MemoryStorageAdapter.js +367 -0
  50. package/dist_ts/congodb/storage/OpLog.d.ts +93 -0
  51. package/dist_ts/congodb/storage/OpLog.js +221 -0
  52. package/dist_ts/congodb/types/interfaces.d.ts +363 -0
  53. package/dist_ts/congodb/types/interfaces.js +2 -0
  54. package/dist_ts/index.d.ts +1 -0
  55. package/dist_ts/index.js +8 -6
  56. package/npmextra.json +17 -7
  57. package/package.json +20 -12
  58. package/readme.hints.md +79 -0
  59. package/readme.md +398 -44
  60. package/ts/00_commitinfo_data.ts +1 -1
  61. package/ts/congodb/congodb.plugins.ts +17 -0
  62. package/ts/congodb/engine/AggregationEngine.ts +283 -0
  63. package/ts/congodb/engine/IndexEngine.ts +479 -0
  64. package/ts/congodb/engine/QueryEngine.ts +301 -0
  65. package/ts/congodb/engine/TransactionEngine.ts +351 -0
  66. package/ts/congodb/engine/UpdateEngine.ts +506 -0
  67. package/ts/congodb/errors/CongoErrors.ts +181 -0
  68. package/ts/congodb/index.ts +37 -0
  69. package/ts/congodb/server/CommandRouter.ts +180 -0
  70. package/ts/congodb/server/CongoServer.ts +298 -0
  71. package/ts/congodb/server/WireProtocol.ts +416 -0
  72. package/ts/congodb/server/handlers/AdminHandler.ts +614 -0
  73. package/ts/congodb/server/handlers/AggregateHandler.ts +342 -0
  74. package/ts/congodb/server/handlers/DeleteHandler.ts +100 -0
  75. package/ts/congodb/server/handlers/FindHandler.ts +301 -0
  76. package/ts/congodb/server/handlers/HelloHandler.ts +78 -0
  77. package/ts/congodb/server/handlers/IndexHandler.ts +207 -0
  78. package/ts/congodb/server/handlers/InsertHandler.ts +91 -0
  79. package/ts/congodb/server/handlers/UpdateHandler.ts +315 -0
  80. package/ts/congodb/server/handlers/index.ts +10 -0
  81. package/ts/congodb/server/index.ts +10 -0
  82. package/ts/congodb/storage/FileStorageAdapter.ts +479 -0
  83. package/ts/congodb/storage/IStorageAdapter.ts +202 -0
  84. package/ts/congodb/storage/MemoryStorageAdapter.ts +443 -0
  85. package/ts/congodb/storage/OpLog.ts +282 -0
  86. package/ts/congodb/types/interfaces.ts +433 -0
  87. package/ts/index.ts +3 -0
@@ -0,0 +1,479 @@
1
+ import * as plugins from '../congodb.plugins.js';
2
+ import type { IStorageAdapter } from '../storage/IStorageAdapter.js';
3
+ import type {
4
+ Document,
5
+ IStoredDocument,
6
+ IIndexSpecification,
7
+ IIndexInfo,
8
+ ICreateIndexOptions,
9
+ } from '../types/interfaces.js';
10
+ import { CongoDuplicateKeyError, CongoIndexError } from '../errors/CongoErrors.js';
11
+ import { QueryEngine } from './QueryEngine.js';
12
+
13
+ /**
14
+ * Index data structure for fast lookups
15
+ */
16
+ interface IIndexData {
17
+ name: string;
18
+ key: Record<string, 1 | -1 | string>;
19
+ unique: boolean;
20
+ sparse: boolean;
21
+ expireAfterSeconds?: number;
22
+ // Map from index key value to document _id(s)
23
+ entries: Map<string, Set<string>>;
24
+ }
25
+
26
+ /**
27
+ * Index engine for managing indexes and query optimization
28
+ */
29
+ export class IndexEngine {
30
+ private dbName: string;
31
+ private collName: string;
32
+ private storage: IStorageAdapter;
33
+ private indexes: Map<string, IIndexData> = new Map();
34
+ private initialized = false;
35
+
36
+ constructor(dbName: string, collName: string, storage: IStorageAdapter) {
37
+ this.dbName = dbName;
38
+ this.collName = collName;
39
+ this.storage = storage;
40
+ }
41
+
42
+ /**
43
+ * Initialize indexes from storage
44
+ */
45
+ async initialize(): Promise<void> {
46
+ if (this.initialized) return;
47
+
48
+ const storedIndexes = await this.storage.getIndexes(this.dbName, this.collName);
49
+ const documents = await this.storage.findAll(this.dbName, this.collName);
50
+
51
+ for (const indexSpec of storedIndexes) {
52
+ const indexData: IIndexData = {
53
+ name: indexSpec.name,
54
+ key: indexSpec.key,
55
+ unique: indexSpec.unique || false,
56
+ sparse: indexSpec.sparse || false,
57
+ expireAfterSeconds: indexSpec.expireAfterSeconds,
58
+ entries: new Map(),
59
+ };
60
+
61
+ // Build index entries
62
+ for (const doc of documents) {
63
+ const keyValue = this.extractKeyValue(doc, indexSpec.key);
64
+ if (keyValue !== null || !indexData.sparse) {
65
+ const keyStr = JSON.stringify(keyValue);
66
+ if (!indexData.entries.has(keyStr)) {
67
+ indexData.entries.set(keyStr, new Set());
68
+ }
69
+ indexData.entries.get(keyStr)!.add(doc._id.toHexString());
70
+ }
71
+ }
72
+
73
+ this.indexes.set(indexSpec.name, indexData);
74
+ }
75
+
76
+ this.initialized = true;
77
+ }
78
+
79
+ /**
80
+ * Create a new index
81
+ */
82
+ async createIndex(
83
+ key: Record<string, 1 | -1 | 'text' | '2dsphere'>,
84
+ options?: ICreateIndexOptions
85
+ ): Promise<string> {
86
+ await this.initialize();
87
+
88
+ // Generate index name if not provided
89
+ const name = options?.name || this.generateIndexName(key);
90
+
91
+ // Check if index already exists
92
+ if (this.indexes.has(name)) {
93
+ return name;
94
+ }
95
+
96
+ // Create index data structure
97
+ const indexData: IIndexData = {
98
+ name,
99
+ key: key as Record<string, 1 | -1 | string>,
100
+ unique: options?.unique || false,
101
+ sparse: options?.sparse || false,
102
+ expireAfterSeconds: options?.expireAfterSeconds,
103
+ entries: new Map(),
104
+ };
105
+
106
+ // Build index from existing documents
107
+ const documents = await this.storage.findAll(this.dbName, this.collName);
108
+
109
+ for (const doc of documents) {
110
+ const keyValue = this.extractKeyValue(doc, key);
111
+
112
+ if (keyValue === null && indexData.sparse) {
113
+ continue;
114
+ }
115
+
116
+ const keyStr = JSON.stringify(keyValue);
117
+
118
+ if (indexData.unique && indexData.entries.has(keyStr)) {
119
+ throw new CongoDuplicateKeyError(
120
+ `E11000 duplicate key error index: ${this.dbName}.${this.collName}.$${name}`,
121
+ key as Record<string, 1>,
122
+ keyValue
123
+ );
124
+ }
125
+
126
+ if (!indexData.entries.has(keyStr)) {
127
+ indexData.entries.set(keyStr, new Set());
128
+ }
129
+ indexData.entries.get(keyStr)!.add(doc._id.toHexString());
130
+ }
131
+
132
+ // Store index
133
+ this.indexes.set(name, indexData);
134
+ await this.storage.saveIndex(this.dbName, this.collName, name, {
135
+ key,
136
+ unique: options?.unique,
137
+ sparse: options?.sparse,
138
+ expireAfterSeconds: options?.expireAfterSeconds,
139
+ });
140
+
141
+ return name;
142
+ }
143
+
144
+ /**
145
+ * Drop an index
146
+ */
147
+ async dropIndex(name: string): Promise<void> {
148
+ await this.initialize();
149
+
150
+ if (name === '_id_') {
151
+ throw new CongoIndexError('cannot drop _id index');
152
+ }
153
+
154
+ if (!this.indexes.has(name)) {
155
+ throw new CongoIndexError(`index not found: ${name}`);
156
+ }
157
+
158
+ this.indexes.delete(name);
159
+ await this.storage.dropIndex(this.dbName, this.collName, name);
160
+ }
161
+
162
+ /**
163
+ * Drop all indexes except _id
164
+ */
165
+ async dropAllIndexes(): Promise<void> {
166
+ await this.initialize();
167
+
168
+ const names = Array.from(this.indexes.keys()).filter(n => n !== '_id_');
169
+ for (const name of names) {
170
+ this.indexes.delete(name);
171
+ await this.storage.dropIndex(this.dbName, this.collName, name);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * List all indexes
177
+ */
178
+ async listIndexes(): Promise<IIndexInfo[]> {
179
+ await this.initialize();
180
+
181
+ return Array.from(this.indexes.values()).map(idx => ({
182
+ v: 2,
183
+ key: idx.key,
184
+ name: idx.name,
185
+ unique: idx.unique || undefined,
186
+ sparse: idx.sparse || undefined,
187
+ expireAfterSeconds: idx.expireAfterSeconds,
188
+ }));
189
+ }
190
+
191
+ /**
192
+ * Check if an index exists
193
+ */
194
+ async indexExists(name: string): Promise<boolean> {
195
+ await this.initialize();
196
+ return this.indexes.has(name);
197
+ }
198
+
199
+ /**
200
+ * Update index entries after document insert
201
+ */
202
+ async onInsert(doc: IStoredDocument): Promise<void> {
203
+ await this.initialize();
204
+
205
+ for (const [name, indexData] of this.indexes) {
206
+ const keyValue = this.extractKeyValue(doc, indexData.key);
207
+
208
+ if (keyValue === null && indexData.sparse) {
209
+ continue;
210
+ }
211
+
212
+ const keyStr = JSON.stringify(keyValue);
213
+
214
+ // Check unique constraint
215
+ if (indexData.unique) {
216
+ const existing = indexData.entries.get(keyStr);
217
+ if (existing && existing.size > 0) {
218
+ throw new CongoDuplicateKeyError(
219
+ `E11000 duplicate key error collection: ${this.dbName}.${this.collName} index: ${name}`,
220
+ indexData.key as Record<string, 1>,
221
+ keyValue
222
+ );
223
+ }
224
+ }
225
+
226
+ if (!indexData.entries.has(keyStr)) {
227
+ indexData.entries.set(keyStr, new Set());
228
+ }
229
+ indexData.entries.get(keyStr)!.add(doc._id.toHexString());
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Update index entries after document update
235
+ */
236
+ async onUpdate(oldDoc: IStoredDocument, newDoc: IStoredDocument): Promise<void> {
237
+ await this.initialize();
238
+
239
+ for (const [name, indexData] of this.indexes) {
240
+ const oldKeyValue = this.extractKeyValue(oldDoc, indexData.key);
241
+ const newKeyValue = this.extractKeyValue(newDoc, indexData.key);
242
+ const oldKeyStr = JSON.stringify(oldKeyValue);
243
+ const newKeyStr = JSON.stringify(newKeyValue);
244
+
245
+ // Remove old entry if key changed
246
+ if (oldKeyStr !== newKeyStr) {
247
+ if (oldKeyValue !== null || !indexData.sparse) {
248
+ const oldSet = indexData.entries.get(oldKeyStr);
249
+ if (oldSet) {
250
+ oldSet.delete(oldDoc._id.toHexString());
251
+ if (oldSet.size === 0) {
252
+ indexData.entries.delete(oldKeyStr);
253
+ }
254
+ }
255
+ }
256
+
257
+ // Add new entry
258
+ if (newKeyValue !== null || !indexData.sparse) {
259
+ // Check unique constraint
260
+ if (indexData.unique) {
261
+ const existing = indexData.entries.get(newKeyStr);
262
+ if (existing && existing.size > 0) {
263
+ throw new CongoDuplicateKeyError(
264
+ `E11000 duplicate key error collection: ${this.dbName}.${this.collName} index: ${name}`,
265
+ indexData.key as Record<string, 1>,
266
+ newKeyValue
267
+ );
268
+ }
269
+ }
270
+
271
+ if (!indexData.entries.has(newKeyStr)) {
272
+ indexData.entries.set(newKeyStr, new Set());
273
+ }
274
+ indexData.entries.get(newKeyStr)!.add(newDoc._id.toHexString());
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Update index entries after document delete
282
+ */
283
+ async onDelete(doc: IStoredDocument): Promise<void> {
284
+ await this.initialize();
285
+
286
+ for (const indexData of this.indexes.values()) {
287
+ const keyValue = this.extractKeyValue(doc, indexData.key);
288
+
289
+ if (keyValue === null && indexData.sparse) {
290
+ continue;
291
+ }
292
+
293
+ const keyStr = JSON.stringify(keyValue);
294
+ const set = indexData.entries.get(keyStr);
295
+ if (set) {
296
+ set.delete(doc._id.toHexString());
297
+ if (set.size === 0) {
298
+ indexData.entries.delete(keyStr);
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Find the best index for a query
306
+ */
307
+ selectIndex(filter: Document): { name: string; data: IIndexData } | null {
308
+ if (!filter || Object.keys(filter).length === 0) {
309
+ return null;
310
+ }
311
+
312
+ // Get filter fields
313
+ const filterFields = new Set(this.getFilterFields(filter));
314
+
315
+ // Score each index
316
+ let bestIndex: { name: string; data: IIndexData } | null = null;
317
+ let bestScore = 0;
318
+
319
+ for (const [name, indexData] of this.indexes) {
320
+ const indexFields = Object.keys(indexData.key);
321
+ let score = 0;
322
+
323
+ // Count how many index fields are in the filter
324
+ for (const field of indexFields) {
325
+ if (filterFields.has(field)) {
326
+ score++;
327
+ } else {
328
+ break; // Index fields must be contiguous
329
+ }
330
+ }
331
+
332
+ // Prefer unique indexes
333
+ if (indexData.unique && score > 0) {
334
+ score += 0.5;
335
+ }
336
+
337
+ if (score > bestScore) {
338
+ bestScore = score;
339
+ bestIndex = { name, data: indexData };
340
+ }
341
+ }
342
+
343
+ return bestIndex;
344
+ }
345
+
346
+ /**
347
+ * Use index to find candidate document IDs
348
+ */
349
+ async findCandidateIds(filter: Document): Promise<Set<string> | null> {
350
+ await this.initialize();
351
+
352
+ const index = this.selectIndex(filter);
353
+ if (!index) return null;
354
+
355
+ // Try to use the index for equality matches
356
+ const indexFields = Object.keys(index.data.key);
357
+ const equalityValues: Record<string, any> = {};
358
+
359
+ for (const field of indexFields) {
360
+ const filterValue = this.getFilterValue(filter, field);
361
+ if (filterValue === undefined) break;
362
+
363
+ // Only use equality matches for index lookup
364
+ if (typeof filterValue === 'object' && filterValue !== null) {
365
+ if (filterValue.$eq !== undefined) {
366
+ equalityValues[field] = filterValue.$eq;
367
+ } else if (filterValue.$in !== undefined) {
368
+ // Handle $in with multiple lookups
369
+ const results = new Set<string>();
370
+ for (const val of filterValue.$in) {
371
+ equalityValues[field] = val;
372
+ const keyStr = JSON.stringify(this.buildKeyValue(equalityValues, index.data.key));
373
+ const ids = index.data.entries.get(keyStr);
374
+ if (ids) {
375
+ for (const id of ids) {
376
+ results.add(id);
377
+ }
378
+ }
379
+ }
380
+ return results;
381
+ } else {
382
+ break; // Non-equality operator, stop here
383
+ }
384
+ } else {
385
+ equalityValues[field] = filterValue;
386
+ }
387
+ }
388
+
389
+ if (Object.keys(equalityValues).length === 0) {
390
+ return null;
391
+ }
392
+
393
+ const keyStr = JSON.stringify(this.buildKeyValue(equalityValues, index.data.key));
394
+ return index.data.entries.get(keyStr) || new Set();
395
+ }
396
+
397
+ // ============================================================================
398
+ // Helper Methods
399
+ // ============================================================================
400
+
401
+ private generateIndexName(key: Record<string, any>): string {
402
+ return Object.entries(key)
403
+ .map(([field, dir]) => `${field}_${dir}`)
404
+ .join('_');
405
+ }
406
+
407
+ private extractKeyValue(doc: Document, key: Record<string, any>): any {
408
+ const values: any[] = [];
409
+
410
+ for (const field of Object.keys(key)) {
411
+ const value = QueryEngine.getNestedValue(doc, field);
412
+ values.push(value === undefined ? null : value);
413
+ }
414
+
415
+ // For single-field index, return the value directly
416
+ if (values.length === 1) {
417
+ return values[0];
418
+ }
419
+
420
+ return values;
421
+ }
422
+
423
+ private buildKeyValue(values: Record<string, any>, key: Record<string, any>): any {
424
+ const result: any[] = [];
425
+
426
+ for (const field of Object.keys(key)) {
427
+ result.push(values[field] !== undefined ? values[field] : null);
428
+ }
429
+
430
+ if (result.length === 1) {
431
+ return result[0];
432
+ }
433
+
434
+ return result;
435
+ }
436
+
437
+ private getFilterFields(filter: Document, prefix = ''): string[] {
438
+ const fields: string[] = [];
439
+
440
+ for (const [key, value] of Object.entries(filter)) {
441
+ if (key.startsWith('$')) {
442
+ // Logical operator
443
+ if (key === '$and' || key === '$or' || key === '$nor') {
444
+ for (const subFilter of value as Document[]) {
445
+ fields.push(...this.getFilterFields(subFilter, prefix));
446
+ }
447
+ }
448
+ } else {
449
+ const fullKey = prefix ? `${prefix}.${key}` : key;
450
+ fields.push(fullKey);
451
+
452
+ // Check for nested filters
453
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
454
+ const subKeys = Object.keys(value);
455
+ if (subKeys.length > 0 && !subKeys[0].startsWith('$')) {
456
+ fields.push(...this.getFilterFields(value, fullKey));
457
+ }
458
+ }
459
+ }
460
+ }
461
+
462
+ return fields;
463
+ }
464
+
465
+ private getFilterValue(filter: Document, field: string): any {
466
+ // Handle dot notation
467
+ const parts = field.split('.');
468
+ let current: any = filter;
469
+
470
+ for (const part of parts) {
471
+ if (current === null || current === undefined) {
472
+ return undefined;
473
+ }
474
+ current = current[part];
475
+ }
476
+
477
+ return current;
478
+ }
479
+ }