@push.rocks/smartmongo 2.2.0 → 4.0.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 +1 -1
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/index.js +3 -3
- package/dist_ts/tsmdb/engine/AggregationEngine.js +189 -0
- package/dist_ts/{congodb → tsmdb}/engine/IndexEngine.d.ts +23 -3
- package/dist_ts/tsmdb/engine/IndexEngine.js +678 -0
- package/dist_ts/tsmdb/engine/QueryEngine.js +271 -0
- package/dist_ts/tsmdb/engine/QueryPlanner.d.ts +64 -0
- package/dist_ts/tsmdb/engine/QueryPlanner.js +308 -0
- package/dist_ts/tsmdb/engine/SessionEngine.d.ts +117 -0
- package/dist_ts/tsmdb/engine/SessionEngine.js +232 -0
- package/dist_ts/{congodb → tsmdb}/engine/TransactionEngine.d.ts +1 -1
- package/dist_ts/tsmdb/engine/TransactionEngine.js +287 -0
- package/dist_ts/tsmdb/engine/UpdateEngine.js +461 -0
- package/dist_ts/{congodb/errors/CongoErrors.d.ts → tsmdb/errors/TsmdbErrors.d.ts} +16 -16
- package/dist_ts/tsmdb/errors/TsmdbErrors.js +155 -0
- package/dist_ts/{congodb → tsmdb}/index.d.ts +11 -4
- package/dist_ts/tsmdb/index.js +31 -0
- package/dist_ts/tsmdb/server/CommandRouter.d.ts +87 -0
- package/dist_ts/tsmdb/server/CommandRouter.js +222 -0
- package/dist_ts/{congodb/server/CongoServer.d.ts → tsmdb/server/TsmdbServer.d.ts} +6 -6
- package/dist_ts/tsmdb/server/TsmdbServer.js +229 -0
- package/dist_ts/{congodb → tsmdb}/server/WireProtocol.d.ts +1 -1
- package/dist_ts/tsmdb/server/WireProtocol.js +298 -0
- package/dist_ts/{congodb → tsmdb}/server/handlers/AdminHandler.d.ts +1 -1
- package/dist_ts/tsmdb/server/handlers/AdminHandler.js +668 -0
- package/dist_ts/{congodb → tsmdb}/server/handlers/AggregateHandler.d.ts +1 -1
- package/dist_ts/tsmdb/server/handlers/AggregateHandler.js +277 -0
- package/dist_ts/{congodb → tsmdb}/server/handlers/DeleteHandler.d.ts +1 -1
- package/dist_ts/tsmdb/server/handlers/DeleteHandler.js +95 -0
- package/dist_ts/{congodb → tsmdb}/server/handlers/FindHandler.d.ts +1 -1
- package/dist_ts/tsmdb/server/handlers/FindHandler.js +291 -0
- package/dist_ts/{congodb → tsmdb}/server/handlers/HelloHandler.d.ts +1 -1
- package/dist_ts/{congodb → tsmdb}/server/handlers/HelloHandler.js +2 -2
- package/dist_ts/{congodb → tsmdb}/server/handlers/IndexHandler.d.ts +1 -1
- package/dist_ts/tsmdb/server/handlers/IndexHandler.js +183 -0
- package/dist_ts/{congodb → tsmdb}/server/handlers/InsertHandler.d.ts +1 -1
- package/dist_ts/tsmdb/server/handlers/InsertHandler.js +79 -0
- package/dist_ts/{congodb → tsmdb}/server/handlers/UpdateHandler.d.ts +1 -1
- package/dist_ts/tsmdb/server/handlers/UpdateHandler.js +296 -0
- package/dist_ts/tsmdb/server/handlers/index.js +10 -0
- package/dist_ts/{congodb → tsmdb}/server/index.d.ts +2 -2
- package/dist_ts/tsmdb/server/index.js +7 -0
- package/dist_ts/{congodb → tsmdb}/storage/FileStorageAdapter.d.ts +27 -3
- package/dist_ts/tsmdb/storage/FileStorageAdapter.js +465 -0
- package/dist_ts/{congodb → tsmdb}/storage/IStorageAdapter.d.ts +7 -2
- package/dist_ts/{congodb → tsmdb}/storage/IStorageAdapter.js +1 -1
- package/dist_ts/{congodb → tsmdb}/storage/MemoryStorageAdapter.d.ts +3 -2
- package/dist_ts/tsmdb/storage/MemoryStorageAdapter.js +378 -0
- package/dist_ts/{congodb → tsmdb}/storage/OpLog.d.ts +1 -1
- package/dist_ts/tsmdb/storage/OpLog.js +221 -0
- package/dist_ts/tsmdb/storage/WAL.d.ts +117 -0
- package/dist_ts/tsmdb/storage/WAL.js +286 -0
- package/dist_ts/tsmdb/tsmdb.plugins.js +14 -0
- package/dist_ts/{congodb → tsmdb}/types/interfaces.d.ts +3 -3
- package/dist_ts/{congodb → tsmdb}/types/interfaces.js +1 -1
- package/dist_ts/tsmdb/utils/checksum.d.ts +30 -0
- package/dist_ts/tsmdb/utils/checksum.js +77 -0
- package/dist_ts/tsmdb/utils/index.d.ts +1 -0
- package/dist_ts/tsmdb/utils/index.js +2 -0
- package/package.json +1 -1
- package/readme.hints.md +7 -12
- package/readme.md +25 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +2 -2
- package/ts/{congodb → tsmdb}/engine/AggregationEngine.ts +1 -1
- package/ts/tsmdb/engine/IndexEngine.ts +798 -0
- package/ts/{congodb → tsmdb}/engine/QueryEngine.ts +1 -1
- package/ts/tsmdb/engine/QueryPlanner.ts +393 -0
- package/ts/tsmdb/engine/SessionEngine.ts +292 -0
- package/ts/{congodb → tsmdb}/engine/TransactionEngine.ts +12 -12
- package/ts/{congodb → tsmdb}/engine/UpdateEngine.ts +1 -1
- package/ts/{congodb/errors/CongoErrors.ts → tsmdb/errors/TsmdbErrors.ts} +34 -34
- package/ts/{congodb → tsmdb}/index.ts +16 -7
- package/ts/{congodb → tsmdb}/server/CommandRouter.ts +114 -5
- package/ts/{congodb/server/CongoServer.ts → tsmdb/server/TsmdbServer.ts} +11 -8
- package/ts/{congodb → tsmdb}/server/WireProtocol.ts +1 -1
- package/ts/{congodb → tsmdb}/server/handlers/AdminHandler.ts +116 -11
- package/ts/{congodb → tsmdb}/server/handlers/AggregateHandler.ts +1 -1
- package/ts/{congodb → tsmdb}/server/handlers/DeleteHandler.ts +18 -3
- package/ts/{congodb → tsmdb}/server/handlers/FindHandler.ts +43 -14
- package/ts/{congodb → tsmdb}/server/handlers/HelloHandler.ts +1 -1
- package/ts/{congodb → tsmdb}/server/handlers/IndexHandler.ts +1 -1
- package/ts/{congodb → tsmdb}/server/handlers/InsertHandler.ts +7 -1
- package/ts/{congodb → tsmdb}/server/handlers/UpdateHandler.ts +34 -5
- package/ts/{congodb → tsmdb}/server/index.ts +2 -2
- package/ts/{congodb → tsmdb}/storage/FileStorageAdapter.ts +90 -7
- package/ts/{congodb → tsmdb}/storage/IStorageAdapter.ts +8 -2
- package/ts/{congodb → tsmdb}/storage/MemoryStorageAdapter.ts +14 -2
- package/ts/{congodb → tsmdb}/storage/OpLog.ts +1 -1
- package/ts/tsmdb/storage/WAL.ts +375 -0
- package/ts/{congodb → tsmdb}/types/interfaces.ts +3 -3
- package/ts/tsmdb/utils/checksum.ts +88 -0
- package/ts/tsmdb/utils/index.ts +1 -0
- package/dist_ts/congodb/congodb.plugins.js +0 -14
- package/dist_ts/congodb/engine/AggregationEngine.js +0 -189
- package/dist_ts/congodb/engine/IndexEngine.js +0 -376
- package/dist_ts/congodb/engine/QueryEngine.js +0 -271
- package/dist_ts/congodb/engine/TransactionEngine.js +0 -287
- package/dist_ts/congodb/engine/UpdateEngine.js +0 -461
- package/dist_ts/congodb/errors/CongoErrors.js +0 -155
- package/dist_ts/congodb/index.js +0 -26
- package/dist_ts/congodb/server/CommandRouter.d.ts +0 -51
- package/dist_ts/congodb/server/CommandRouter.js +0 -132
- package/dist_ts/congodb/server/CongoServer.js +0 -227
- package/dist_ts/congodb/server/WireProtocol.js +0 -298
- package/dist_ts/congodb/server/handlers/AdminHandler.js +0 -568
- package/dist_ts/congodb/server/handlers/AggregateHandler.js +0 -277
- package/dist_ts/congodb/server/handlers/DeleteHandler.js +0 -83
- package/dist_ts/congodb/server/handlers/FindHandler.js +0 -261
- package/dist_ts/congodb/server/handlers/IndexHandler.js +0 -183
- package/dist_ts/congodb/server/handlers/InsertHandler.js +0 -76
- package/dist_ts/congodb/server/handlers/UpdateHandler.js +0 -270
- package/dist_ts/congodb/server/handlers/index.js +0 -10
- package/dist_ts/congodb/server/index.js +0 -7
- package/dist_ts/congodb/storage/FileStorageAdapter.js +0 -396
- package/dist_ts/congodb/storage/MemoryStorageAdapter.js +0 -367
- package/dist_ts/congodb/storage/OpLog.js +0 -221
- package/ts/congodb/engine/IndexEngine.ts +0 -479
- /package/dist_ts/{congodb → tsmdb}/engine/AggregationEngine.d.ts +0 -0
- /package/dist_ts/{congodb → tsmdb}/engine/QueryEngine.d.ts +0 -0
- /package/dist_ts/{congodb → tsmdb}/engine/UpdateEngine.d.ts +0 -0
- /package/dist_ts/{congodb → tsmdb}/server/handlers/index.d.ts +0 -0
- /package/dist_ts/{congodb/congodb.plugins.d.ts → tsmdb/tsmdb.plugins.d.ts} +0 -0
- /package/ts/{congodb → tsmdb}/server/handlers/index.ts +0 -0
- /package/ts/{congodb/congodb.plugins.ts → tsmdb/tsmdb.plugins.ts} +0 -0
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
import * as plugins from '../tsmdb.plugins.js';
|
|
2
|
+
import type { IStorageAdapter } from '../storage/IStorageAdapter.js';
|
|
3
|
+
|
|
4
|
+
// Simple B-Tree implementation for range queries
|
|
5
|
+
// Since sorted-btree has ESM/CJS interop issues, we use a simple custom implementation
|
|
6
|
+
class SimpleBTree<K, V> {
|
|
7
|
+
private entries: Map<string, { key: K; value: V }> = new Map();
|
|
8
|
+
private sortedKeys: K[] = [];
|
|
9
|
+
private comparator: (a: K, b: K) => number;
|
|
10
|
+
|
|
11
|
+
constructor(_unused?: undefined, comparator?: (a: K, b: K) => number) {
|
|
12
|
+
this.comparator = comparator || ((a: K, b: K) => {
|
|
13
|
+
if (a < b) return -1;
|
|
14
|
+
if (a > b) return 1;
|
|
15
|
+
return 0;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private keyToString(key: K): string {
|
|
20
|
+
return JSON.stringify(key);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set(key: K, value: V): boolean {
|
|
24
|
+
const keyStr = this.keyToString(key);
|
|
25
|
+
const existed = this.entries.has(keyStr);
|
|
26
|
+
this.entries.set(keyStr, { key, value });
|
|
27
|
+
|
|
28
|
+
if (!existed) {
|
|
29
|
+
// Insert in sorted order
|
|
30
|
+
const idx = this.sortedKeys.findIndex(k => this.comparator(k, key) > 0);
|
|
31
|
+
if (idx === -1) {
|
|
32
|
+
this.sortedKeys.push(key);
|
|
33
|
+
} else {
|
|
34
|
+
this.sortedKeys.splice(idx, 0, key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return !existed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get(key: K): V | undefined {
|
|
41
|
+
const entry = this.entries.get(this.keyToString(key));
|
|
42
|
+
return entry?.value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
delete(key: K): boolean {
|
|
46
|
+
const keyStr = this.keyToString(key);
|
|
47
|
+
if (this.entries.has(keyStr)) {
|
|
48
|
+
this.entries.delete(keyStr);
|
|
49
|
+
const idx = this.sortedKeys.findIndex(k => this.comparator(k, key) === 0);
|
|
50
|
+
if (idx !== -1) {
|
|
51
|
+
this.sortedKeys.splice(idx, 1);
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
forRange(
|
|
59
|
+
lowKey: K | undefined,
|
|
60
|
+
highKey: K | undefined,
|
|
61
|
+
lowInclusive: boolean,
|
|
62
|
+
highInclusive: boolean,
|
|
63
|
+
callback: (value: V, key: K) => void
|
|
64
|
+
): void {
|
|
65
|
+
for (const key of this.sortedKeys) {
|
|
66
|
+
// Check low bound
|
|
67
|
+
if (lowKey !== undefined) {
|
|
68
|
+
const cmp = this.comparator(key, lowKey);
|
|
69
|
+
if (cmp < 0) continue;
|
|
70
|
+
if (cmp === 0 && !lowInclusive) continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check high bound
|
|
74
|
+
if (highKey !== undefined) {
|
|
75
|
+
const cmp = this.comparator(key, highKey);
|
|
76
|
+
if (cmp > 0) break;
|
|
77
|
+
if (cmp === 0 && !highInclusive) break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const entry = this.entries.get(this.keyToString(key));
|
|
81
|
+
if (entry) {
|
|
82
|
+
callback(entry.value, key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
import type {
|
|
88
|
+
Document,
|
|
89
|
+
IStoredDocument,
|
|
90
|
+
IIndexSpecification,
|
|
91
|
+
IIndexInfo,
|
|
92
|
+
ICreateIndexOptions,
|
|
93
|
+
} from '../types/interfaces.js';
|
|
94
|
+
import { TsmdbDuplicateKeyError, TsmdbIndexError } from '../errors/TsmdbErrors.js';
|
|
95
|
+
import { QueryEngine } from './QueryEngine.js';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Comparator for B-Tree that handles mixed types consistently
|
|
99
|
+
*/
|
|
100
|
+
function indexKeyComparator(a: any, b: any): number {
|
|
101
|
+
// Handle null/undefined
|
|
102
|
+
if (a === null || a === undefined) {
|
|
103
|
+
if (b === null || b === undefined) return 0;
|
|
104
|
+
return -1;
|
|
105
|
+
}
|
|
106
|
+
if (b === null || b === undefined) return 1;
|
|
107
|
+
|
|
108
|
+
// Handle arrays (compound keys)
|
|
109
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
110
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
111
|
+
const cmp = indexKeyComparator(a[i], b[i]);
|
|
112
|
+
if (cmp !== 0) return cmp;
|
|
113
|
+
}
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle ObjectId
|
|
118
|
+
if (a instanceof plugins.bson.ObjectId && b instanceof plugins.bson.ObjectId) {
|
|
119
|
+
return a.toHexString().localeCompare(b.toHexString());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle Date
|
|
123
|
+
if (a instanceof Date && b instanceof Date) {
|
|
124
|
+
return a.getTime() - b.getTime();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle different types - use type ordering (null < number < string < object)
|
|
128
|
+
const typeOrder = (v: any): number => {
|
|
129
|
+
if (v === null || v === undefined) return 0;
|
|
130
|
+
if (typeof v === 'number') return 1;
|
|
131
|
+
if (typeof v === 'string') return 2;
|
|
132
|
+
if (typeof v === 'boolean') return 3;
|
|
133
|
+
if (v instanceof Date) return 4;
|
|
134
|
+
if (v instanceof plugins.bson.ObjectId) return 5;
|
|
135
|
+
return 6;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const typeA = typeOrder(a);
|
|
139
|
+
const typeB = typeOrder(b);
|
|
140
|
+
if (typeA !== typeB) return typeA - typeB;
|
|
141
|
+
|
|
142
|
+
// Same type comparison
|
|
143
|
+
if (typeof a === 'number') return a - b;
|
|
144
|
+
if (typeof a === 'string') return a.localeCompare(b);
|
|
145
|
+
if (typeof a === 'boolean') return (a ? 1 : 0) - (b ? 1 : 0);
|
|
146
|
+
|
|
147
|
+
// Fallback to string comparison
|
|
148
|
+
return String(a).localeCompare(String(b));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Index data structure using B-Tree for range queries
|
|
153
|
+
*/
|
|
154
|
+
interface IIndexData {
|
|
155
|
+
name: string;
|
|
156
|
+
key: Record<string, 1 | -1 | string>;
|
|
157
|
+
unique: boolean;
|
|
158
|
+
sparse: boolean;
|
|
159
|
+
expireAfterSeconds?: number;
|
|
160
|
+
// B-Tree for ordered index lookups (supports range queries)
|
|
161
|
+
btree: SimpleBTree<any, Set<string>>;
|
|
162
|
+
// Hash map for fast equality lookups
|
|
163
|
+
hashMap: Map<string, Set<string>>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Index engine for managing indexes and query optimization
|
|
168
|
+
*/
|
|
169
|
+
export class IndexEngine {
|
|
170
|
+
private dbName: string;
|
|
171
|
+
private collName: string;
|
|
172
|
+
private storage: IStorageAdapter;
|
|
173
|
+
private indexes: Map<string, IIndexData> = new Map();
|
|
174
|
+
private initialized = false;
|
|
175
|
+
|
|
176
|
+
constructor(dbName: string, collName: string, storage: IStorageAdapter) {
|
|
177
|
+
this.dbName = dbName;
|
|
178
|
+
this.collName = collName;
|
|
179
|
+
this.storage = storage;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Initialize indexes from storage
|
|
184
|
+
*/
|
|
185
|
+
async initialize(): Promise<void> {
|
|
186
|
+
if (this.initialized) return;
|
|
187
|
+
|
|
188
|
+
const storedIndexes = await this.storage.getIndexes(this.dbName, this.collName);
|
|
189
|
+
const documents = await this.storage.findAll(this.dbName, this.collName);
|
|
190
|
+
|
|
191
|
+
for (const indexSpec of storedIndexes) {
|
|
192
|
+
const indexData: IIndexData = {
|
|
193
|
+
name: indexSpec.name,
|
|
194
|
+
key: indexSpec.key,
|
|
195
|
+
unique: indexSpec.unique || false,
|
|
196
|
+
sparse: indexSpec.sparse || false,
|
|
197
|
+
expireAfterSeconds: indexSpec.expireAfterSeconds,
|
|
198
|
+
btree: new SimpleBTree<any, Set<string>>(undefined, indexKeyComparator),
|
|
199
|
+
hashMap: new Map(),
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Build index entries
|
|
203
|
+
for (const doc of documents) {
|
|
204
|
+
const keyValue = this.extractKeyValue(doc, indexSpec.key);
|
|
205
|
+
if (keyValue !== null || !indexData.sparse) {
|
|
206
|
+
const keyStr = JSON.stringify(keyValue);
|
|
207
|
+
|
|
208
|
+
// Add to hash map
|
|
209
|
+
if (!indexData.hashMap.has(keyStr)) {
|
|
210
|
+
indexData.hashMap.set(keyStr, new Set());
|
|
211
|
+
}
|
|
212
|
+
indexData.hashMap.get(keyStr)!.add(doc._id.toHexString());
|
|
213
|
+
|
|
214
|
+
// Add to B-tree
|
|
215
|
+
const existing = indexData.btree.get(keyValue);
|
|
216
|
+
if (existing) {
|
|
217
|
+
existing.add(doc._id.toHexString());
|
|
218
|
+
} else {
|
|
219
|
+
indexData.btree.set(keyValue, new Set([doc._id.toHexString()]));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.indexes.set(indexSpec.name, indexData);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.initialized = true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create a new index
|
|
232
|
+
*/
|
|
233
|
+
async createIndex(
|
|
234
|
+
key: Record<string, 1 | -1 | 'text' | '2dsphere'>,
|
|
235
|
+
options?: ICreateIndexOptions
|
|
236
|
+
): Promise<string> {
|
|
237
|
+
await this.initialize();
|
|
238
|
+
|
|
239
|
+
// Generate index name if not provided
|
|
240
|
+
const name = options?.name || this.generateIndexName(key);
|
|
241
|
+
|
|
242
|
+
// Check if index already exists
|
|
243
|
+
if (this.indexes.has(name)) {
|
|
244
|
+
return name;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Create index data structure
|
|
248
|
+
const indexData: IIndexData = {
|
|
249
|
+
name,
|
|
250
|
+
key: key as Record<string, 1 | -1 | string>,
|
|
251
|
+
unique: options?.unique || false,
|
|
252
|
+
sparse: options?.sparse || false,
|
|
253
|
+
expireAfterSeconds: options?.expireAfterSeconds,
|
|
254
|
+
btree: new SimpleBTree<any, Set<string>>(undefined, indexKeyComparator),
|
|
255
|
+
hashMap: new Map(),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Build index from existing documents
|
|
259
|
+
const documents = await this.storage.findAll(this.dbName, this.collName);
|
|
260
|
+
|
|
261
|
+
for (const doc of documents) {
|
|
262
|
+
const keyValue = this.extractKeyValue(doc, key);
|
|
263
|
+
|
|
264
|
+
if (keyValue === null && indexData.sparse) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const keyStr = JSON.stringify(keyValue);
|
|
269
|
+
|
|
270
|
+
if (indexData.unique && indexData.hashMap.has(keyStr)) {
|
|
271
|
+
throw new TsmdbDuplicateKeyError(
|
|
272
|
+
`E11000 duplicate key error index: ${this.dbName}.${this.collName}.$${name}`,
|
|
273
|
+
key as Record<string, 1>,
|
|
274
|
+
keyValue
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Add to hash map
|
|
279
|
+
if (!indexData.hashMap.has(keyStr)) {
|
|
280
|
+
indexData.hashMap.set(keyStr, new Set());
|
|
281
|
+
}
|
|
282
|
+
indexData.hashMap.get(keyStr)!.add(doc._id.toHexString());
|
|
283
|
+
|
|
284
|
+
// Add to B-tree
|
|
285
|
+
const existing = indexData.btree.get(keyValue);
|
|
286
|
+
if (existing) {
|
|
287
|
+
existing.add(doc._id.toHexString());
|
|
288
|
+
} else {
|
|
289
|
+
indexData.btree.set(keyValue, new Set([doc._id.toHexString()]));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Store index
|
|
294
|
+
this.indexes.set(name, indexData);
|
|
295
|
+
await this.storage.saveIndex(this.dbName, this.collName, name, {
|
|
296
|
+
key,
|
|
297
|
+
unique: options?.unique,
|
|
298
|
+
sparse: options?.sparse,
|
|
299
|
+
expireAfterSeconds: options?.expireAfterSeconds,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return name;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Drop an index
|
|
307
|
+
*/
|
|
308
|
+
async dropIndex(name: string): Promise<void> {
|
|
309
|
+
await this.initialize();
|
|
310
|
+
|
|
311
|
+
if (name === '_id_') {
|
|
312
|
+
throw new TsmdbIndexError('cannot drop _id index');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!this.indexes.has(name)) {
|
|
316
|
+
throw new TsmdbIndexError(`index not found: ${name}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.indexes.delete(name);
|
|
320
|
+
await this.storage.dropIndex(this.dbName, this.collName, name);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Drop all indexes except _id
|
|
325
|
+
*/
|
|
326
|
+
async dropAllIndexes(): Promise<void> {
|
|
327
|
+
await this.initialize();
|
|
328
|
+
|
|
329
|
+
const names = Array.from(this.indexes.keys()).filter(n => n !== '_id_');
|
|
330
|
+
for (const name of names) {
|
|
331
|
+
this.indexes.delete(name);
|
|
332
|
+
await this.storage.dropIndex(this.dbName, this.collName, name);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* List all indexes
|
|
338
|
+
*/
|
|
339
|
+
async listIndexes(): Promise<IIndexInfo[]> {
|
|
340
|
+
await this.initialize();
|
|
341
|
+
|
|
342
|
+
return Array.from(this.indexes.values()).map(idx => ({
|
|
343
|
+
v: 2,
|
|
344
|
+
key: idx.key,
|
|
345
|
+
name: idx.name,
|
|
346
|
+
unique: idx.unique || undefined,
|
|
347
|
+
sparse: idx.sparse || undefined,
|
|
348
|
+
expireAfterSeconds: idx.expireAfterSeconds,
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Check if an index exists
|
|
354
|
+
*/
|
|
355
|
+
async indexExists(name: string): Promise<boolean> {
|
|
356
|
+
await this.initialize();
|
|
357
|
+
return this.indexes.has(name);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Update index entries after document insert
|
|
362
|
+
*/
|
|
363
|
+
async onInsert(doc: IStoredDocument): Promise<void> {
|
|
364
|
+
await this.initialize();
|
|
365
|
+
|
|
366
|
+
for (const [name, indexData] of this.indexes) {
|
|
367
|
+
const keyValue = this.extractKeyValue(doc, indexData.key);
|
|
368
|
+
|
|
369
|
+
if (keyValue === null && indexData.sparse) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const keyStr = JSON.stringify(keyValue);
|
|
374
|
+
|
|
375
|
+
// Check unique constraint
|
|
376
|
+
if (indexData.unique) {
|
|
377
|
+
const existing = indexData.hashMap.get(keyStr);
|
|
378
|
+
if (existing && existing.size > 0) {
|
|
379
|
+
throw new TsmdbDuplicateKeyError(
|
|
380
|
+
`E11000 duplicate key error collection: ${this.dbName}.${this.collName} index: ${name}`,
|
|
381
|
+
indexData.key as Record<string, 1>,
|
|
382
|
+
keyValue
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Add to hash map
|
|
388
|
+
if (!indexData.hashMap.has(keyStr)) {
|
|
389
|
+
indexData.hashMap.set(keyStr, new Set());
|
|
390
|
+
}
|
|
391
|
+
indexData.hashMap.get(keyStr)!.add(doc._id.toHexString());
|
|
392
|
+
|
|
393
|
+
// Add to B-tree
|
|
394
|
+
const btreeSet = indexData.btree.get(keyValue);
|
|
395
|
+
if (btreeSet) {
|
|
396
|
+
btreeSet.add(doc._id.toHexString());
|
|
397
|
+
} else {
|
|
398
|
+
indexData.btree.set(keyValue, new Set([doc._id.toHexString()]));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Update index entries after document update
|
|
405
|
+
*/
|
|
406
|
+
async onUpdate(oldDoc: IStoredDocument, newDoc: IStoredDocument): Promise<void> {
|
|
407
|
+
await this.initialize();
|
|
408
|
+
|
|
409
|
+
for (const [name, indexData] of this.indexes) {
|
|
410
|
+
const oldKeyValue = this.extractKeyValue(oldDoc, indexData.key);
|
|
411
|
+
const newKeyValue = this.extractKeyValue(newDoc, indexData.key);
|
|
412
|
+
const oldKeyStr = JSON.stringify(oldKeyValue);
|
|
413
|
+
const newKeyStr = JSON.stringify(newKeyValue);
|
|
414
|
+
|
|
415
|
+
// Remove old entry if key changed
|
|
416
|
+
if (oldKeyStr !== newKeyStr) {
|
|
417
|
+
if (oldKeyValue !== null || !indexData.sparse) {
|
|
418
|
+
// Remove from hash map
|
|
419
|
+
const oldHashSet = indexData.hashMap.get(oldKeyStr);
|
|
420
|
+
if (oldHashSet) {
|
|
421
|
+
oldHashSet.delete(oldDoc._id.toHexString());
|
|
422
|
+
if (oldHashSet.size === 0) {
|
|
423
|
+
indexData.hashMap.delete(oldKeyStr);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Remove from B-tree
|
|
428
|
+
const oldBtreeSet = indexData.btree.get(oldKeyValue);
|
|
429
|
+
if (oldBtreeSet) {
|
|
430
|
+
oldBtreeSet.delete(oldDoc._id.toHexString());
|
|
431
|
+
if (oldBtreeSet.size === 0) {
|
|
432
|
+
indexData.btree.delete(oldKeyValue);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Add new entry
|
|
438
|
+
if (newKeyValue !== null || !indexData.sparse) {
|
|
439
|
+
// Check unique constraint
|
|
440
|
+
if (indexData.unique) {
|
|
441
|
+
const existing = indexData.hashMap.get(newKeyStr);
|
|
442
|
+
if (existing && existing.size > 0) {
|
|
443
|
+
throw new TsmdbDuplicateKeyError(
|
|
444
|
+
`E11000 duplicate key error collection: ${this.dbName}.${this.collName} index: ${name}`,
|
|
445
|
+
indexData.key as Record<string, 1>,
|
|
446
|
+
newKeyValue
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Add to hash map
|
|
452
|
+
if (!indexData.hashMap.has(newKeyStr)) {
|
|
453
|
+
indexData.hashMap.set(newKeyStr, new Set());
|
|
454
|
+
}
|
|
455
|
+
indexData.hashMap.get(newKeyStr)!.add(newDoc._id.toHexString());
|
|
456
|
+
|
|
457
|
+
// Add to B-tree
|
|
458
|
+
const newBtreeSet = indexData.btree.get(newKeyValue);
|
|
459
|
+
if (newBtreeSet) {
|
|
460
|
+
newBtreeSet.add(newDoc._id.toHexString());
|
|
461
|
+
} else {
|
|
462
|
+
indexData.btree.set(newKeyValue, new Set([newDoc._id.toHexString()]));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Update index entries after document delete
|
|
471
|
+
*/
|
|
472
|
+
async onDelete(doc: IStoredDocument): Promise<void> {
|
|
473
|
+
await this.initialize();
|
|
474
|
+
|
|
475
|
+
for (const indexData of this.indexes.values()) {
|
|
476
|
+
const keyValue = this.extractKeyValue(doc, indexData.key);
|
|
477
|
+
|
|
478
|
+
if (keyValue === null && indexData.sparse) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const keyStr = JSON.stringify(keyValue);
|
|
483
|
+
|
|
484
|
+
// Remove from hash map
|
|
485
|
+
const hashSet = indexData.hashMap.get(keyStr);
|
|
486
|
+
if (hashSet) {
|
|
487
|
+
hashSet.delete(doc._id.toHexString());
|
|
488
|
+
if (hashSet.size === 0) {
|
|
489
|
+
indexData.hashMap.delete(keyStr);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Remove from B-tree
|
|
494
|
+
const btreeSet = indexData.btree.get(keyValue);
|
|
495
|
+
if (btreeSet) {
|
|
496
|
+
btreeSet.delete(doc._id.toHexString());
|
|
497
|
+
if (btreeSet.size === 0) {
|
|
498
|
+
indexData.btree.delete(keyValue);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Find the best index for a query
|
|
506
|
+
*/
|
|
507
|
+
selectIndex(filter: Document): { name: string; data: IIndexData } | null {
|
|
508
|
+
if (!filter || Object.keys(filter).length === 0) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Get filter fields and operators
|
|
513
|
+
const filterInfo = this.analyzeFilter(filter);
|
|
514
|
+
|
|
515
|
+
// Score each index
|
|
516
|
+
let bestIndex: { name: string; data: IIndexData } | null = null;
|
|
517
|
+
let bestScore = 0;
|
|
518
|
+
|
|
519
|
+
for (const [name, indexData] of this.indexes) {
|
|
520
|
+
const indexFields = Object.keys(indexData.key);
|
|
521
|
+
let score = 0;
|
|
522
|
+
|
|
523
|
+
// Count how many index fields can be used
|
|
524
|
+
for (const field of indexFields) {
|
|
525
|
+
const info = filterInfo.get(field);
|
|
526
|
+
if (!info) break;
|
|
527
|
+
|
|
528
|
+
// Equality is best
|
|
529
|
+
if (info.equality) {
|
|
530
|
+
score += 2;
|
|
531
|
+
} else if (info.range) {
|
|
532
|
+
// Range queries can use B-tree
|
|
533
|
+
score += 1;
|
|
534
|
+
} else if (info.in) {
|
|
535
|
+
score += 1.5;
|
|
536
|
+
} else {
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Prefer unique indexes
|
|
542
|
+
if (indexData.unique && score > 0) {
|
|
543
|
+
score += 0.5;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (score > bestScore) {
|
|
547
|
+
bestScore = score;
|
|
548
|
+
bestIndex = { name, data: indexData };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return bestIndex;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Analyze filter to extract field operators
|
|
557
|
+
*/
|
|
558
|
+
private analyzeFilter(filter: Document): Map<string, { equality: boolean; range: boolean; in: boolean; ops: Record<string, any> }> {
|
|
559
|
+
const result = new Map<string, { equality: boolean; range: boolean; in: boolean; ops: Record<string, any> }>();
|
|
560
|
+
|
|
561
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
562
|
+
if (key.startsWith('$')) continue;
|
|
563
|
+
|
|
564
|
+
const info = { equality: false, range: false, in: false, ops: {} as Record<string, any> };
|
|
565
|
+
|
|
566
|
+
if (typeof value !== 'object' || value === null || value instanceof plugins.bson.ObjectId || value instanceof Date) {
|
|
567
|
+
info.equality = true;
|
|
568
|
+
info.ops['$eq'] = value;
|
|
569
|
+
} else {
|
|
570
|
+
const ops = value as Record<string, any>;
|
|
571
|
+
if (ops.$eq !== undefined) {
|
|
572
|
+
info.equality = true;
|
|
573
|
+
info.ops['$eq'] = ops.$eq;
|
|
574
|
+
}
|
|
575
|
+
if (ops.$in !== undefined) {
|
|
576
|
+
info.in = true;
|
|
577
|
+
info.ops['$in'] = ops.$in;
|
|
578
|
+
}
|
|
579
|
+
if (ops.$gt !== undefined || ops.$gte !== undefined || ops.$lt !== undefined || ops.$lte !== undefined) {
|
|
580
|
+
info.range = true;
|
|
581
|
+
if (ops.$gt !== undefined) info.ops['$gt'] = ops.$gt;
|
|
582
|
+
if (ops.$gte !== undefined) info.ops['$gte'] = ops.$gte;
|
|
583
|
+
if (ops.$lt !== undefined) info.ops['$lt'] = ops.$lt;
|
|
584
|
+
if (ops.$lte !== undefined) info.ops['$lte'] = ops.$lte;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
result.set(key, info);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Use index to find candidate document IDs (supports range queries with B-tree)
|
|
596
|
+
*/
|
|
597
|
+
async findCandidateIds(filter: Document): Promise<Set<string> | null> {
|
|
598
|
+
await this.initialize();
|
|
599
|
+
|
|
600
|
+
const index = this.selectIndex(filter);
|
|
601
|
+
if (!index) return null;
|
|
602
|
+
|
|
603
|
+
const filterInfo = this.analyzeFilter(filter);
|
|
604
|
+
const indexFields = Object.keys(index.data.key);
|
|
605
|
+
|
|
606
|
+
// For single-field indexes with range queries, use B-tree
|
|
607
|
+
if (indexFields.length === 1) {
|
|
608
|
+
const field = indexFields[0];
|
|
609
|
+
const info = filterInfo.get(field);
|
|
610
|
+
|
|
611
|
+
if (info) {
|
|
612
|
+
// Handle equality using hash map (faster)
|
|
613
|
+
if (info.equality) {
|
|
614
|
+
const keyStr = JSON.stringify(info.ops['$eq']);
|
|
615
|
+
return index.data.hashMap.get(keyStr) || new Set();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Handle $in using hash map
|
|
619
|
+
if (info.in) {
|
|
620
|
+
const results = new Set<string>();
|
|
621
|
+
for (const val of info.ops['$in']) {
|
|
622
|
+
const keyStr = JSON.stringify(val);
|
|
623
|
+
const ids = index.data.hashMap.get(keyStr);
|
|
624
|
+
if (ids) {
|
|
625
|
+
for (const id of ids) {
|
|
626
|
+
results.add(id);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return results;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Handle range queries using B-tree
|
|
634
|
+
if (info.range) {
|
|
635
|
+
return this.findRangeCandidates(index.data, info.ops);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
// For compound indexes, use hash map with partial key matching
|
|
640
|
+
const equalityValues: Record<string, any> = {};
|
|
641
|
+
|
|
642
|
+
for (const field of indexFields) {
|
|
643
|
+
const info = filterInfo.get(field);
|
|
644
|
+
if (!info) break;
|
|
645
|
+
|
|
646
|
+
if (info.equality) {
|
|
647
|
+
equalityValues[field] = info.ops['$eq'];
|
|
648
|
+
} else if (info.in) {
|
|
649
|
+
// Handle $in with multiple lookups
|
|
650
|
+
const results = new Set<string>();
|
|
651
|
+
for (const val of info.ops['$in']) {
|
|
652
|
+
equalityValues[field] = val;
|
|
653
|
+
const keyStr = JSON.stringify(this.buildKeyValue(equalityValues, index.data.key));
|
|
654
|
+
const ids = index.data.hashMap.get(keyStr);
|
|
655
|
+
if (ids) {
|
|
656
|
+
for (const id of ids) {
|
|
657
|
+
results.add(id);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return results;
|
|
662
|
+
} else {
|
|
663
|
+
break; // Non-equality/in operator, stop here
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (Object.keys(equalityValues).length > 0) {
|
|
668
|
+
const keyStr = JSON.stringify(this.buildKeyValue(equalityValues, index.data.key));
|
|
669
|
+
return index.data.hashMap.get(keyStr) || new Set();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Find candidates using B-tree range scan
|
|
678
|
+
*/
|
|
679
|
+
private findRangeCandidates(indexData: IIndexData, ops: Record<string, any>): Set<string> {
|
|
680
|
+
const results = new Set<string>();
|
|
681
|
+
|
|
682
|
+
let lowKey: any = undefined;
|
|
683
|
+
let highKey: any = undefined;
|
|
684
|
+
let lowInclusive = true;
|
|
685
|
+
let highInclusive = true;
|
|
686
|
+
|
|
687
|
+
if (ops['$gt'] !== undefined) {
|
|
688
|
+
lowKey = ops['$gt'];
|
|
689
|
+
lowInclusive = false;
|
|
690
|
+
}
|
|
691
|
+
if (ops['$gte'] !== undefined) {
|
|
692
|
+
lowKey = ops['$gte'];
|
|
693
|
+
lowInclusive = true;
|
|
694
|
+
}
|
|
695
|
+
if (ops['$lt'] !== undefined) {
|
|
696
|
+
highKey = ops['$lt'];
|
|
697
|
+
highInclusive = false;
|
|
698
|
+
}
|
|
699
|
+
if (ops['$lte'] !== undefined) {
|
|
700
|
+
highKey = ops['$lte'];
|
|
701
|
+
highInclusive = true;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Use B-tree range iteration
|
|
705
|
+
indexData.btree.forRange(lowKey, highKey, lowInclusive, highInclusive, (value, key) => {
|
|
706
|
+
if (value) {
|
|
707
|
+
for (const id of value) {
|
|
708
|
+
results.add(id);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
return results;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ============================================================================
|
|
717
|
+
// Helper Methods
|
|
718
|
+
// ============================================================================
|
|
719
|
+
|
|
720
|
+
private generateIndexName(key: Record<string, any>): string {
|
|
721
|
+
return Object.entries(key)
|
|
722
|
+
.map(([field, dir]) => `${field}_${dir}`)
|
|
723
|
+
.join('_');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private extractKeyValue(doc: Document, key: Record<string, any>): any {
|
|
727
|
+
const values: any[] = [];
|
|
728
|
+
|
|
729
|
+
for (const field of Object.keys(key)) {
|
|
730
|
+
const value = QueryEngine.getNestedValue(doc, field);
|
|
731
|
+
values.push(value === undefined ? null : value);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// For single-field index, return the value directly
|
|
735
|
+
if (values.length === 1) {
|
|
736
|
+
return values[0];
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return values;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private buildKeyValue(values: Record<string, any>, key: Record<string, any>): any {
|
|
743
|
+
const result: any[] = [];
|
|
744
|
+
|
|
745
|
+
for (const field of Object.keys(key)) {
|
|
746
|
+
result.push(values[field] !== undefined ? values[field] : null);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (result.length === 1) {
|
|
750
|
+
return result[0];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return result;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
private getFilterFields(filter: Document, prefix = ''): string[] {
|
|
757
|
+
const fields: string[] = [];
|
|
758
|
+
|
|
759
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
760
|
+
if (key.startsWith('$')) {
|
|
761
|
+
// Logical operator
|
|
762
|
+
if (key === '$and' || key === '$or' || key === '$nor') {
|
|
763
|
+
for (const subFilter of value as Document[]) {
|
|
764
|
+
fields.push(...this.getFilterFields(subFilter, prefix));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
769
|
+
fields.push(fullKey);
|
|
770
|
+
|
|
771
|
+
// Check for nested filters
|
|
772
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
773
|
+
const subKeys = Object.keys(value);
|
|
774
|
+
if (subKeys.length > 0 && !subKeys[0].startsWith('$')) {
|
|
775
|
+
fields.push(...this.getFilterFields(value, fullKey));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return fields;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private getFilterValue(filter: Document, field: string): any {
|
|
785
|
+
// Handle dot notation
|
|
786
|
+
const parts = field.split('.');
|
|
787
|
+
let current: any = filter;
|
|
788
|
+
|
|
789
|
+
for (const part of parts) {
|
|
790
|
+
if (current === null || current === undefined) {
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
current = current[part];
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return current;
|
|
797
|
+
}
|
|
798
|
+
}
|