@joystick.js/db-canary 0.0.0-canary.2250 → 0.0.0-canary.2252
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/client/database.js +1 -1
- package/dist/client/index.js +1 -1
- package/dist/server/cluster/master.js +4 -4
- package/dist/server/cluster/worker.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/lib/auto_index_manager.js +1 -1
- package/dist/server/lib/backup_manager.js +1 -1
- package/dist/server/lib/index_manager.js +1 -1
- package/dist/server/lib/operation_dispatcher.js +1 -1
- package/dist/server/lib/operations/admin.js +1 -1
- package/dist/server/lib/operations/bulk_write.js +1 -1
- package/dist/server/lib/operations/create_index.js +1 -1
- package/dist/server/lib/operations/delete_many.js +1 -1
- package/dist/server/lib/operations/delete_one.js +1 -1
- package/dist/server/lib/operations/find.js +1 -1
- package/dist/server/lib/operations/find_one.js +1 -1
- package/dist/server/lib/operations/insert_one.js +1 -1
- package/dist/server/lib/operations/update_one.js +1 -1
- package/dist/server/lib/send_response.js +1 -1
- package/dist/server/lib/tcp_protocol.js +1 -1
- package/package.json +2 -2
- package/src/client/database.js +92 -119
- package/src/client/index.js +279 -345
- package/src/server/cluster/master.js +265 -156
- package/src/server/cluster/worker.js +26 -18
- package/src/server/index.js +553 -330
- package/src/server/lib/auto_index_manager.js +85 -23
- package/src/server/lib/backup_manager.js +117 -70
- package/src/server/lib/index_manager.js +63 -25
- package/src/server/lib/operation_dispatcher.js +339 -168
- package/src/server/lib/operations/admin.js +343 -205
- package/src/server/lib/operations/bulk_write.js +458 -194
- package/src/server/lib/operations/create_index.js +127 -34
- package/src/server/lib/operations/delete_many.js +204 -67
- package/src/server/lib/operations/delete_one.js +164 -52
- package/src/server/lib/operations/find.js +563 -201
- package/src/server/lib/operations/find_one.js +544 -188
- package/src/server/lib/operations/insert_one.js +147 -52
- package/src/server/lib/operations/update_one.js +334 -93
- package/src/server/lib/send_response.js +37 -17
- package/src/server/lib/tcp_protocol.js +158 -53
- package/tests/server/cluster/master_read_write_operations.test.js +5 -14
- package/tests/server/integration/authentication_integration.test.js +18 -10
- package/tests/server/integration/backup_integration.test.js +35 -27
- package/tests/server/lib/api_key_manager.test.js +88 -32
- package/tests/server/lib/development_mode.test.js +2 -2
- package/tests/server/lib/operations/admin.test.js +20 -12
- package/tests/server/lib/operations/delete_one.test.js +10 -4
- package/tests/server/lib/operations/find_array_queries.test.js +261 -0
|
@@ -13,26 +13,99 @@ import create_logger from '../logger.js';
|
|
|
13
13
|
|
|
14
14
|
const { create_context_logger } = create_logger('find_one');
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Splits field path into parts.
|
|
18
|
+
* @param {string} field_path - Dot-separated field path
|
|
19
|
+
* @returns {Array<string>} Array of field parts
|
|
20
|
+
*/
|
|
21
|
+
const split_field_path = (field_path) => {
|
|
22
|
+
return field_path.split('.');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Checks if value is null or undefined.
|
|
27
|
+
* @param {any} value - Value to check
|
|
28
|
+
* @returns {boolean} True if null or undefined
|
|
29
|
+
*/
|
|
30
|
+
const is_null_or_undefined = (value) => {
|
|
31
|
+
return value === null || value === undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extracts remaining path from field parts.
|
|
36
|
+
* @param {Array<string>} parts - Field path parts
|
|
37
|
+
* @param {number} current_index - Current index in parts
|
|
38
|
+
* @returns {string} Remaining path
|
|
39
|
+
*/
|
|
40
|
+
const get_remaining_path = (parts, current_index) => {
|
|
41
|
+
return parts.slice(current_index + 1).join('.');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Collects nested values from array elements.
|
|
46
|
+
* @param {Array} array_value - Array to process
|
|
47
|
+
* @param {string} remaining_path - Remaining field path
|
|
48
|
+
* @returns {Array} Collected nested values
|
|
49
|
+
*/
|
|
50
|
+
const collect_nested_values_from_array = (array_value, remaining_path) => {
|
|
51
|
+
const nested_values = [];
|
|
52
|
+
|
|
53
|
+
for (let j = 0; j < array_value.length; j++) {
|
|
54
|
+
const item = array_value[j];
|
|
55
|
+
if (typeof item === 'object' && item !== null) {
|
|
56
|
+
const nested_value = get_field_value(item, remaining_path);
|
|
57
|
+
if (nested_value !== undefined) {
|
|
58
|
+
if (Array.isArray(nested_value)) {
|
|
59
|
+
nested_values.push(...nested_value);
|
|
60
|
+
} else {
|
|
61
|
+
nested_values.push(nested_value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return nested_values.length > 0 ? nested_values : undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
16
70
|
/**
|
|
17
71
|
* Gets the value of a nested field from a document using dot notation.
|
|
72
|
+
* Handles nested array object queries like 'reviews.rating' where reviews is an array of objects.
|
|
18
73
|
* @param {Object} document - Document to extract field value from
|
|
19
|
-
* @param {string} field_path - Dot-separated field path (e.g., 'user.name')
|
|
20
|
-
* @returns {any} Field value or undefined if not found
|
|
74
|
+
* @param {string} field_path - Dot-separated field path (e.g., 'user.name', 'reviews.rating')
|
|
75
|
+
* @returns {any} Field value, array of values for nested array queries, or undefined if not found
|
|
21
76
|
*/
|
|
22
77
|
const get_field_value = (document, field_path) => {
|
|
23
|
-
const parts = field_path
|
|
78
|
+
const parts = split_field_path(field_path);
|
|
24
79
|
let value = document;
|
|
25
80
|
|
|
26
|
-
for (
|
|
27
|
-
|
|
81
|
+
for (let i = 0; i < parts.length; i++) {
|
|
82
|
+
const part = parts[i];
|
|
83
|
+
|
|
84
|
+
if (is_null_or_undefined(value)) {
|
|
28
85
|
return undefined;
|
|
29
86
|
}
|
|
87
|
+
|
|
30
88
|
value = value[part];
|
|
89
|
+
|
|
90
|
+
if (Array.isArray(value) && i < parts.length - 1) {
|
|
91
|
+
const remaining_path = get_remaining_path(parts, i);
|
|
92
|
+
return collect_nested_values_from_array(value, remaining_path);
|
|
93
|
+
}
|
|
31
94
|
}
|
|
32
95
|
|
|
33
96
|
return value;
|
|
34
97
|
};
|
|
35
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Checks if current object has the specified property.
|
|
101
|
+
* @param {Object} current - Current object
|
|
102
|
+
* @param {string} property - Property name
|
|
103
|
+
* @returns {boolean} True if property exists
|
|
104
|
+
*/
|
|
105
|
+
const has_property = (current, property) => {
|
|
106
|
+
return current.hasOwnProperty(property);
|
|
107
|
+
};
|
|
108
|
+
|
|
36
109
|
/**
|
|
37
110
|
* Checks if a nested field exists in a document using dot notation.
|
|
38
111
|
* @param {Object} document - Document to check
|
|
@@ -40,16 +113,16 @@ const get_field_value = (document, field_path) => {
|
|
|
40
113
|
* @returns {boolean} True if field exists
|
|
41
114
|
*/
|
|
42
115
|
const field_exists = (document, field_path) => {
|
|
43
|
-
const parts = field_path
|
|
116
|
+
const parts = split_field_path(field_path);
|
|
44
117
|
let current = document;
|
|
45
118
|
|
|
46
119
|
for (let i = 0; i < parts.length; i++) {
|
|
47
|
-
if (current
|
|
120
|
+
if (is_null_or_undefined(current) || typeof current !== 'object') {
|
|
48
121
|
return false;
|
|
49
122
|
}
|
|
50
123
|
|
|
51
124
|
if (i === parts.length - 1) {
|
|
52
|
-
return current
|
|
125
|
+
return has_property(current, parts[i]);
|
|
53
126
|
}
|
|
54
127
|
|
|
55
128
|
current = current[parts[i]];
|
|
@@ -58,24 +131,265 @@ const field_exists = (document, field_path) => {
|
|
|
58
131
|
return false;
|
|
59
132
|
};
|
|
60
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Checks if array contains value for equality operator.
|
|
136
|
+
* @param {Array} field_value - Array field value
|
|
137
|
+
* @param {any} operand - Value to check
|
|
138
|
+
* @returns {boolean} True if array contains value
|
|
139
|
+
*/
|
|
140
|
+
const array_contains_value = (field_value, operand) => {
|
|
141
|
+
return field_value.includes(operand);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Checks if any array element satisfies comparison operator.
|
|
146
|
+
* @param {Array} field_value - Array field value
|
|
147
|
+
* @param {any} operand - Value to compare
|
|
148
|
+
* @param {Function} comparator - Comparison function
|
|
149
|
+
* @returns {boolean} True if any element satisfies condition
|
|
150
|
+
*/
|
|
151
|
+
const any_array_element_satisfies = (field_value, operand, comparator) => {
|
|
152
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
153
|
+
if (comparator(field_value[i], operand)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Handles equality operator matching.
|
|
162
|
+
* @param {any} field_value - Field value to test
|
|
163
|
+
* @param {any} operand - Value to match
|
|
164
|
+
* @returns {boolean} True if matches
|
|
165
|
+
*/
|
|
166
|
+
const handle_equality_operator = (field_value, operand) => {
|
|
167
|
+
if (Array.isArray(field_value)) {
|
|
168
|
+
return array_contains_value(field_value, operand);
|
|
169
|
+
}
|
|
170
|
+
return field_value === operand;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Handles not equal operator matching.
|
|
175
|
+
* @param {any} field_value - Field value to test
|
|
176
|
+
* @param {any} operand - Value to match
|
|
177
|
+
* @returns {boolean} True if does not match
|
|
178
|
+
*/
|
|
179
|
+
const handle_not_equal_operator = (field_value, operand) => {
|
|
180
|
+
if (Array.isArray(field_value)) {
|
|
181
|
+
return !array_contains_value(field_value, operand);
|
|
182
|
+
}
|
|
183
|
+
return field_value !== operand;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Handles greater than operator matching.
|
|
188
|
+
* @param {any} field_value - Field value to test
|
|
189
|
+
* @param {any} operand - Value to compare
|
|
190
|
+
* @returns {boolean} True if greater than
|
|
191
|
+
*/
|
|
192
|
+
const handle_greater_than_operator = (field_value, operand) => {
|
|
193
|
+
if (Array.isArray(field_value)) {
|
|
194
|
+
return any_array_element_satisfies(field_value, operand, (val, op) => val > op);
|
|
195
|
+
}
|
|
196
|
+
return field_value > operand;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Handles greater than or equal operator matching.
|
|
201
|
+
* @param {any} field_value - Field value to test
|
|
202
|
+
* @param {any} operand - Value to compare
|
|
203
|
+
* @returns {boolean} True if greater than or equal
|
|
204
|
+
*/
|
|
205
|
+
const handle_greater_than_equal_operator = (field_value, operand) => {
|
|
206
|
+
if (Array.isArray(field_value)) {
|
|
207
|
+
return any_array_element_satisfies(field_value, operand, (val, op) => val >= op);
|
|
208
|
+
}
|
|
209
|
+
return field_value >= operand;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Handles less than operator matching.
|
|
214
|
+
* @param {any} field_value - Field value to test
|
|
215
|
+
* @param {any} operand - Value to compare
|
|
216
|
+
* @returns {boolean} True if less than
|
|
217
|
+
*/
|
|
218
|
+
const handle_less_than_operator = (field_value, operand) => {
|
|
219
|
+
if (Array.isArray(field_value)) {
|
|
220
|
+
return any_array_element_satisfies(field_value, operand, (val, op) => val < op);
|
|
221
|
+
}
|
|
222
|
+
return field_value < operand;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Handles less than or equal operator matching.
|
|
227
|
+
* @param {any} field_value - Field value to test
|
|
228
|
+
* @param {any} operand - Value to compare
|
|
229
|
+
* @returns {boolean} True if less than or equal
|
|
230
|
+
*/
|
|
231
|
+
const handle_less_than_equal_operator = (field_value, operand) => {
|
|
232
|
+
if (Array.isArray(field_value)) {
|
|
233
|
+
return any_array_element_satisfies(field_value, operand, (val, op) => val <= op);
|
|
234
|
+
}
|
|
235
|
+
return field_value <= operand;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Handles in operator matching.
|
|
240
|
+
* @param {any} field_value - Field value to test
|
|
241
|
+
* @param {Array} operand - Array of values to match
|
|
242
|
+
* @returns {boolean} True if value is in array
|
|
243
|
+
*/
|
|
244
|
+
const handle_in_operator = (field_value, operand) => {
|
|
245
|
+
if (!Array.isArray(operand)) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (Array.isArray(field_value)) {
|
|
250
|
+
return any_array_element_satisfies(field_value, operand, (val, op) => op.includes(val));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return operand.includes(field_value);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Handles not in operator matching.
|
|
258
|
+
* @param {any} field_value - Field value to test
|
|
259
|
+
* @param {Array} operand - Array of values to exclude
|
|
260
|
+
* @returns {boolean} True if value is not in array
|
|
261
|
+
*/
|
|
262
|
+
const handle_not_in_operator = (field_value, operand) => {
|
|
263
|
+
if (!Array.isArray(operand)) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (Array.isArray(field_value)) {
|
|
268
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
269
|
+
if (operand.includes(field_value[i])) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return !operand.includes(field_value);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handles exists operator matching.
|
|
281
|
+
* @param {Object} document - Document to check
|
|
282
|
+
* @param {string} field - Field name
|
|
283
|
+
* @param {boolean} operand - Whether field should exist
|
|
284
|
+
* @returns {boolean} True if existence matches expectation
|
|
285
|
+
*/
|
|
286
|
+
const handle_exists_operator = (document, field, operand) => {
|
|
287
|
+
const exists = field_exists(document, field);
|
|
288
|
+
return operand ? exists : !exists;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Handles regex operator matching.
|
|
293
|
+
* @param {any} field_value - Field value to test
|
|
294
|
+
* @param {string} pattern - Regex pattern
|
|
295
|
+
* @param {string} options - Regex options
|
|
296
|
+
* @returns {boolean} True if matches regex
|
|
297
|
+
*/
|
|
298
|
+
const handle_regex_operator = (field_value, pattern, options = '') => {
|
|
299
|
+
const regex = new RegExp(pattern, options);
|
|
300
|
+
|
|
301
|
+
if (Array.isArray(field_value)) {
|
|
302
|
+
return any_array_element_satisfies(field_value, regex, (val, reg) => {
|
|
303
|
+
return typeof val === 'string' && reg.test(val);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return regex.test(field_value);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Processes query operators for a field.
|
|
312
|
+
* @param {Object} document - Document to test
|
|
313
|
+
* @param {string} field - Field name
|
|
314
|
+
* @param {any} field_value - Field value
|
|
315
|
+
* @param {Object} value - Query value with operators
|
|
316
|
+
* @returns {boolean} True if all operators match
|
|
317
|
+
*/
|
|
318
|
+
const process_query_operators = (document, field, field_value, value) => {
|
|
319
|
+
for (const [operator, operand] of Object.entries(value)) {
|
|
320
|
+
switch (operator) {
|
|
321
|
+
case '$eq':
|
|
322
|
+
if (!handle_equality_operator(field_value, operand)) return false;
|
|
323
|
+
break;
|
|
324
|
+
case '$ne':
|
|
325
|
+
if (!handle_not_equal_operator(field_value, operand)) return false;
|
|
326
|
+
break;
|
|
327
|
+
case '$gt':
|
|
328
|
+
if (!handle_greater_than_operator(field_value, operand)) return false;
|
|
329
|
+
break;
|
|
330
|
+
case '$gte':
|
|
331
|
+
if (!handle_greater_than_equal_operator(field_value, operand)) return false;
|
|
332
|
+
break;
|
|
333
|
+
case '$lt':
|
|
334
|
+
if (!handle_less_than_operator(field_value, operand)) return false;
|
|
335
|
+
break;
|
|
336
|
+
case '$lte':
|
|
337
|
+
if (!handle_less_than_equal_operator(field_value, operand)) return false;
|
|
338
|
+
break;
|
|
339
|
+
case '$in':
|
|
340
|
+
if (!handle_in_operator(field_value, operand)) return false;
|
|
341
|
+
break;
|
|
342
|
+
case '$nin':
|
|
343
|
+
if (!handle_not_in_operator(field_value, operand)) return false;
|
|
344
|
+
break;
|
|
345
|
+
case '$exists':
|
|
346
|
+
if (!handle_exists_operator(document, field, operand)) return false;
|
|
347
|
+
break;
|
|
348
|
+
case '$regex':
|
|
349
|
+
const regex_options = value.$options || '';
|
|
350
|
+
if (!handle_regex_operator(field_value, operand, regex_options)) return false;
|
|
351
|
+
break;
|
|
352
|
+
case '$options':
|
|
353
|
+
break;
|
|
354
|
+
default:
|
|
355
|
+
throw new Error(`Unsupported query operator: ${operator}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Handles OR operator matching.
|
|
363
|
+
* @param {Object} document - Document to test
|
|
364
|
+
* @param {Array} or_conditions - Array of OR conditions
|
|
365
|
+
* @returns {boolean} True if any condition matches
|
|
366
|
+
*/
|
|
367
|
+
const handle_or_operator = (document, or_conditions) => {
|
|
368
|
+
for (let i = 0; i < or_conditions.length; i++) {
|
|
369
|
+
if (matches_filter(document, or_conditions[i])) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return false;
|
|
374
|
+
};
|
|
375
|
+
|
|
61
376
|
/**
|
|
62
377
|
* Checks if a document matches the given filter criteria.
|
|
378
|
+
* Supports MongoDB-like query operators with optimized array field matching.
|
|
63
379
|
* @param {Object} document - Document to test
|
|
64
380
|
* @param {Object} filter - Filter criteria with MongoDB-style operators
|
|
65
381
|
* @returns {boolean} True if document matches filter
|
|
66
|
-
* @throws {Error} When unsupported query operator is used
|
|
67
382
|
*/
|
|
68
383
|
const matches_filter = (document, filter) => {
|
|
69
384
|
if (!filter || Object.keys(filter).length === 0) {
|
|
70
385
|
return true;
|
|
71
386
|
}
|
|
72
387
|
|
|
73
|
-
// Handle $or operator at the top level
|
|
74
388
|
if (filter.$or && Array.isArray(filter.$or)) {
|
|
75
|
-
|
|
76
|
-
|
|
389
|
+
if (!handle_or_operator(document, filter.$or)) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
77
392
|
|
|
78
|
-
// Continue checking other conditions (if any) outside of $or
|
|
79
393
|
const remaining_filter = { ...filter };
|
|
80
394
|
delete remaining_filter.$or;
|
|
81
395
|
if (Object.keys(remaining_filter).length > 0) {
|
|
@@ -88,62 +402,70 @@ const matches_filter = (document, filter) => {
|
|
|
88
402
|
const field_value = get_field_value(document, field);
|
|
89
403
|
|
|
90
404
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
case '$eq':
|
|
94
|
-
if (field_value !== operand) return false;
|
|
95
|
-
break;
|
|
96
|
-
case '$ne':
|
|
97
|
-
if (field_value === operand) return false;
|
|
98
|
-
break;
|
|
99
|
-
case '$gt':
|
|
100
|
-
if (field_value <= operand) return false;
|
|
101
|
-
break;
|
|
102
|
-
case '$gte':
|
|
103
|
-
if (field_value < operand) return false;
|
|
104
|
-
break;
|
|
105
|
-
case '$lt':
|
|
106
|
-
if (field_value >= operand) return false;
|
|
107
|
-
break;
|
|
108
|
-
case '$lte':
|
|
109
|
-
if (field_value > operand) return false;
|
|
110
|
-
break;
|
|
111
|
-
case '$in':
|
|
112
|
-
if (!Array.isArray(operand) || !operand.includes(field_value)) return false;
|
|
113
|
-
break;
|
|
114
|
-
case '$nin':
|
|
115
|
-
if (!Array.isArray(operand) || operand.includes(field_value)) return false;
|
|
116
|
-
break;
|
|
117
|
-
case '$exists':
|
|
118
|
-
const exists = field_exists(document, field);
|
|
119
|
-
if (operand && !exists) return false;
|
|
120
|
-
if (!operand && exists) return false;
|
|
121
|
-
break;
|
|
122
|
-
case '$regex':
|
|
123
|
-
// Handle $options parameter for regex flags
|
|
124
|
-
const regex_options = value.$options || '';
|
|
125
|
-
const regex = new RegExp(operand, regex_options);
|
|
126
|
-
if (!regex.test(field_value)) return false;
|
|
127
|
-
break;
|
|
128
|
-
case '$options':
|
|
129
|
-
// $options is handled as part of $regex, skip it here
|
|
130
|
-
break;
|
|
131
|
-
default:
|
|
132
|
-
throw new Error(`Unsupported query operator: ${operator}`);
|
|
133
|
-
}
|
|
405
|
+
if (!process_query_operators(document, field, field_value, value)) {
|
|
406
|
+
return false;
|
|
134
407
|
}
|
|
135
408
|
} else {
|
|
136
|
-
if (field_value
|
|
409
|
+
if (!handle_equality_operator(field_value, value)) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
137
412
|
}
|
|
138
413
|
}
|
|
139
414
|
|
|
140
415
|
return true;
|
|
141
416
|
};
|
|
142
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Determines if projection is inclusion-based.
|
|
420
|
+
* @param {Object} projection - Projection specification
|
|
421
|
+
* @returns {boolean} True if inclusion-based
|
|
422
|
+
*/
|
|
423
|
+
const is_inclusion_projection = (projection) => {
|
|
424
|
+
return Object.values(projection).some(value => value === 1 || value === true);
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Applies inclusion projection to document.
|
|
429
|
+
* @param {Object} document - Document to project
|
|
430
|
+
* @param {Object} projection - Projection specification
|
|
431
|
+
* @returns {Object} Projected document
|
|
432
|
+
*/
|
|
433
|
+
const apply_inclusion_projection = (document, projection) => {
|
|
434
|
+
const projected_document = { _id: document._id };
|
|
435
|
+
|
|
436
|
+
for (const [field, include] of Object.entries(projection)) {
|
|
437
|
+
if (field === '_id' && (include === 0 || include === false)) {
|
|
438
|
+
delete projected_document._id;
|
|
439
|
+
} else if (include === 1 || include === true) {
|
|
440
|
+
projected_document[field] = document[field];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return projected_document;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Applies exclusion projection to document.
|
|
449
|
+
* @param {Object} document - Document to project
|
|
450
|
+
* @param {Object} projection - Projection specification
|
|
451
|
+
* @returns {Object} Projected document
|
|
452
|
+
*/
|
|
453
|
+
const apply_exclusion_projection = (document, projection) => {
|
|
454
|
+
const projected_document = { ...document };
|
|
455
|
+
|
|
456
|
+
for (const [field, exclude] of Object.entries(projection)) {
|
|
457
|
+
if (exclude === 0 || exclude === false) {
|
|
458
|
+
delete projected_document[field];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return projected_document;
|
|
463
|
+
};
|
|
464
|
+
|
|
143
465
|
/**
|
|
144
466
|
* Applies projection to a document, including or excluding specified fields.
|
|
145
467
|
* @param {Object} document - Document to project
|
|
146
|
-
* @param {Object} projection - Projection specification
|
|
468
|
+
* @param {Object} projection - Projection specification
|
|
147
469
|
* @returns {Object} Projected document
|
|
148
470
|
*/
|
|
149
471
|
const apply_projection = (document, projection) => {
|
|
@@ -151,56 +473,183 @@ const apply_projection = (document, projection) => {
|
|
|
151
473
|
return document;
|
|
152
474
|
}
|
|
153
475
|
|
|
154
|
-
|
|
155
|
-
|
|
476
|
+
if (is_inclusion_projection(projection)) {
|
|
477
|
+
return apply_inclusion_projection(document, projection);
|
|
478
|
+
} else {
|
|
479
|
+
return apply_exclusion_projection(document, projection);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Validates required parameters.
|
|
485
|
+
* @param {string} database_name - Database name
|
|
486
|
+
* @param {string} collection_name - Collection name
|
|
487
|
+
* @throws {Error} When parameters are missing
|
|
488
|
+
*/
|
|
489
|
+
const validate_find_parameters = (database_name, collection_name) => {
|
|
490
|
+
if (!database_name) {
|
|
491
|
+
throw new Error('Database name is required');
|
|
492
|
+
}
|
|
156
493
|
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
494
|
+
if (!collection_name) {
|
|
495
|
+
throw new Error('Collection name is required');
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Attempts to parse document from JSON string.
|
|
501
|
+
* @param {string} value - JSON string
|
|
502
|
+
* @returns {Object|null} Parsed document or null
|
|
503
|
+
*/
|
|
504
|
+
const parse_document_safely = (value) => {
|
|
505
|
+
try {
|
|
506
|
+
return JSON.parse(value);
|
|
507
|
+
} catch (parse_error) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Searches for document using index.
|
|
514
|
+
* @param {Object} db - Database instance
|
|
515
|
+
* @param {string} database_name - Database name
|
|
516
|
+
* @param {string} collection_name - Collection name
|
|
517
|
+
* @param {Object} filter - Query filter
|
|
518
|
+
* @param {Object} index_info - Index information
|
|
519
|
+
* @returns {Object|null} Found document or null
|
|
520
|
+
*/
|
|
521
|
+
const search_using_index = (db, database_name, collection_name, filter, index_info) => {
|
|
522
|
+
const { field, operators } = index_info;
|
|
523
|
+
const field_filter = filter[field];
|
|
524
|
+
|
|
525
|
+
record_index_usage(database_name, collection_name, field);
|
|
526
|
+
|
|
527
|
+
let document_ids = null;
|
|
528
|
+
|
|
529
|
+
if (typeof field_filter === 'object' && field_filter !== null && !Array.isArray(field_filter)) {
|
|
530
|
+
for (const operator of operators) {
|
|
531
|
+
if (field_filter[operator] !== undefined) {
|
|
532
|
+
document_ids = find_documents_by_index(database_name, collection_name, field, operator, field_filter[operator]);
|
|
533
|
+
break;
|
|
165
534
|
}
|
|
166
535
|
}
|
|
167
|
-
} else {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
536
|
+
} else if (operators.includes('eq')) {
|
|
537
|
+
document_ids = find_documents_by_index(database_name, collection_name, field, 'eq', field_filter);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (document_ids && document_ids.length > 0) {
|
|
541
|
+
for (const document_id of document_ids) {
|
|
542
|
+
const collection_key = build_collection_key(database_name, collection_name, document_id);
|
|
543
|
+
const value = db.get(collection_key);
|
|
544
|
+
|
|
545
|
+
if (value) {
|
|
546
|
+
const document = parse_document_safely(value);
|
|
547
|
+
if (document && matches_filter(document, filter)) {
|
|
548
|
+
return document;
|
|
549
|
+
}
|
|
173
550
|
}
|
|
174
551
|
}
|
|
175
552
|
}
|
|
176
553
|
|
|
177
|
-
return
|
|
554
|
+
return null;
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Searches collection using full scan.
|
|
559
|
+
* @param {Object} db - Database instance
|
|
560
|
+
* @param {string} database_name - Database name
|
|
561
|
+
* @param {string} collection_name - Collection name
|
|
562
|
+
* @param {Object} filter - Query filter
|
|
563
|
+
* @returns {Object|null} Found document or null
|
|
564
|
+
*/
|
|
565
|
+
const search_using_full_scan = (db, database_name, collection_name, filter) => {
|
|
566
|
+
const collection_prefix = `${database_name}:${collection_name}:`;
|
|
567
|
+
const range = db.getRange({ start: collection_prefix, end: collection_prefix + '\xFF' });
|
|
568
|
+
|
|
569
|
+
for (const { key, value } of range) {
|
|
570
|
+
const document = parse_document_safely(value);
|
|
571
|
+
if (document && matches_filter(document, filter)) {
|
|
572
|
+
return document;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return null;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Records query for auto-indexing analysis.
|
|
581
|
+
* @param {Function} log - Logger function
|
|
582
|
+
* @param {string} collection_name - Collection name
|
|
583
|
+
* @param {Object} filter - Query filter
|
|
584
|
+
* @param {number} execution_time - Execution time in milliseconds
|
|
585
|
+
* @param {boolean} used_index - Whether index was used
|
|
586
|
+
* @param {string} indexed_field - Field that was indexed
|
|
587
|
+
*/
|
|
588
|
+
const record_query_for_auto_indexing = (log, collection_name, filter, execution_time, used_index, indexed_field) => {
|
|
589
|
+
try {
|
|
590
|
+
record_query(collection_name, filter, execution_time, used_index, indexed_field);
|
|
591
|
+
} catch (auto_index_error) {
|
|
592
|
+
log.warn('Failed to record query for auto-indexing', {
|
|
593
|
+
error: auto_index_error.message
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Logs successful document find.
|
|
600
|
+
* @param {Function} log - Logger function
|
|
601
|
+
* @param {string} database_name - Database name
|
|
602
|
+
* @param {string} collection_name - Collection name
|
|
603
|
+
* @param {string} document_id - Document ID
|
|
604
|
+
* @param {boolean} used_index - Whether index was used
|
|
605
|
+
* @param {string} indexed_field - Field that was indexed
|
|
606
|
+
* @param {number} execution_time - Execution time in milliseconds
|
|
607
|
+
*/
|
|
608
|
+
const log_document_found = (log, database_name, collection_name, document_id, used_index, indexed_field, execution_time) => {
|
|
609
|
+
log.info('Document found', {
|
|
610
|
+
database: database_name,
|
|
611
|
+
collection: collection_name,
|
|
612
|
+
document_id,
|
|
613
|
+
used_index,
|
|
614
|
+
indexed_field,
|
|
615
|
+
execution_time_ms: execution_time
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Logs when no document is found.
|
|
621
|
+
* @param {Function} log - Logger function
|
|
622
|
+
* @param {string} database_name - Database name
|
|
623
|
+
* @param {string} collection_name - Collection name
|
|
624
|
+
* @param {boolean} used_index - Whether index was used
|
|
625
|
+
* @param {string} indexed_field - Field that was indexed
|
|
626
|
+
* @param {number} execution_time - Execution time in milliseconds
|
|
627
|
+
*/
|
|
628
|
+
const log_no_document_found = (log, database_name, collection_name, used_index, indexed_field, execution_time) => {
|
|
629
|
+
log.info('No document found', {
|
|
630
|
+
database: database_name,
|
|
631
|
+
collection: collection_name,
|
|
632
|
+
used_index,
|
|
633
|
+
indexed_field,
|
|
634
|
+
execution_time_ms: execution_time
|
|
635
|
+
});
|
|
178
636
|
};
|
|
179
637
|
|
|
180
638
|
/**
|
|
181
639
|
* Finds a single document in a collection matching the filter criteria.
|
|
182
640
|
* @param {string} database_name - Name of the database
|
|
183
641
|
* @param {string} collection_name - Name of the collection
|
|
184
|
-
* @param {Object}
|
|
185
|
-
* @param {Object}
|
|
186
|
-
* @param {Object} [options.projection] - Field projection specification
|
|
187
|
-
* @param {Object} [options.sort] - Sort specification (currently unused)
|
|
642
|
+
* @param {Object} filter - Filter criteria
|
|
643
|
+
* @param {Object} options - Query options
|
|
188
644
|
* @returns {Promise<Object|null>} Found document or null if not found
|
|
189
|
-
* @throws {Error} When database or collection name is missing or query fails
|
|
190
645
|
*/
|
|
191
646
|
const find_one = async (database_name, collection_name, filter = {}, options = {}) => {
|
|
192
647
|
const log = create_context_logger();
|
|
193
648
|
|
|
194
|
-
|
|
195
|
-
throw new Error('Database name is required');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (!collection_name) {
|
|
199
|
-
throw new Error('Collection name is required');
|
|
200
|
-
}
|
|
649
|
+
validate_find_parameters(database_name, collection_name);
|
|
201
650
|
|
|
202
651
|
const db = get_database();
|
|
203
|
-
const { projection
|
|
652
|
+
const { projection } = options;
|
|
204
653
|
const start_time = Date.now();
|
|
205
654
|
|
|
206
655
|
try {
|
|
@@ -211,119 +660,26 @@ const find_one = async (database_name, collection_name, filter = {}, options = {
|
|
|
211
660
|
const index_info = can_use_index(database_name, collection_name, filter);
|
|
212
661
|
|
|
213
662
|
if (index_info) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (typeof field_filter === 'object' && field_filter !== null && !Array.isArray(field_filter)) {
|
|
219
|
-
for (const operator of operators) {
|
|
220
|
-
if (field_filter[operator] !== undefined) {
|
|
221
|
-
const document_ids = find_documents_by_index(database_name, collection_name, field, operator, field_filter[operator]);
|
|
222
|
-
|
|
223
|
-
if (document_ids && document_ids.length > 0) {
|
|
224
|
-
used_index = true;
|
|
225
|
-
record_index_usage(database_name, collection_name, field);
|
|
226
|
-
|
|
227
|
-
for (const document_id of document_ids) {
|
|
228
|
-
const collection_key = build_collection_key(database_name, collection_name, document_id);
|
|
229
|
-
const value = db.get(collection_key);
|
|
230
|
-
|
|
231
|
-
if (value) {
|
|
232
|
-
try {
|
|
233
|
-
const document = JSON.parse(value);
|
|
234
|
-
if (matches_filter(document, filter)) {
|
|
235
|
-
found_document = document;
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
} catch (parse_error) {
|
|
239
|
-
// Skip documents that can't be parsed
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
} else if (operators.includes('eq')) {
|
|
249
|
-
const document_ids = find_documents_by_index(database_name, collection_name, field, 'eq', field_filter);
|
|
250
|
-
|
|
251
|
-
if (document_ids && document_ids.length > 0) {
|
|
252
|
-
used_index = true;
|
|
253
|
-
record_index_usage(database_name, collection_name, field);
|
|
254
|
-
|
|
255
|
-
for (const document_id of document_ids) {
|
|
256
|
-
const collection_key = build_collection_key(database_name, collection_name, document_id);
|
|
257
|
-
const value = db.get(collection_key);
|
|
258
|
-
|
|
259
|
-
if (value) {
|
|
260
|
-
try {
|
|
261
|
-
const document = JSON.parse(value);
|
|
262
|
-
if (matches_filter(document, filter)) {
|
|
263
|
-
found_document = document;
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
} catch (parse_error) {
|
|
267
|
-
// Skip documents that can't be parsed
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
663
|
+
indexed_field = index_info.field;
|
|
664
|
+
found_document = search_using_index(db, database_name, collection_name, filter, index_info);
|
|
665
|
+
used_index = found_document !== null;
|
|
274
666
|
}
|
|
275
667
|
|
|
276
668
|
if (!used_index) {
|
|
277
|
-
|
|
278
|
-
const range = db.getRange({ start: collection_prefix, end: collection_prefix + '\xFF' });
|
|
279
|
-
|
|
280
|
-
for (const { key, value } of range) {
|
|
281
|
-
try {
|
|
282
|
-
const document = JSON.parse(value);
|
|
283
|
-
if (matches_filter(document, filter)) {
|
|
284
|
-
found_document = document;
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
} catch (parse_error) {
|
|
288
|
-
// Skip documents that can't be parsed
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
669
|
+
found_document = search_using_full_scan(db, database_name, collection_name, filter);
|
|
292
670
|
}
|
|
293
671
|
|
|
294
672
|
const execution_time = Date.now() - start_time;
|
|
295
673
|
|
|
296
|
-
|
|
297
|
-
record_query(collection_name, filter, execution_time, used_index, indexed_field);
|
|
298
|
-
} catch (auto_index_error) {
|
|
299
|
-
log.warn('Failed to record query for auto-indexing', {
|
|
300
|
-
error: auto_index_error.message
|
|
301
|
-
});
|
|
302
|
-
}
|
|
674
|
+
record_query_for_auto_indexing(log, collection_name, filter, execution_time, used_index, indexed_field);
|
|
303
675
|
|
|
304
676
|
if (found_document) {
|
|
305
677
|
const projected_document = apply_projection(found_document, projection);
|
|
306
|
-
|
|
307
|
-
log.info('Document found', {
|
|
308
|
-
database: database_name,
|
|
309
|
-
collection: collection_name,
|
|
310
|
-
document_id: found_document._id,
|
|
311
|
-
used_index,
|
|
312
|
-
indexed_field,
|
|
313
|
-
execution_time_ms: execution_time
|
|
314
|
-
});
|
|
315
|
-
|
|
678
|
+
log_document_found(log, database_name, collection_name, found_document._id, used_index, indexed_field, execution_time);
|
|
316
679
|
return projected_document;
|
|
317
680
|
}
|
|
318
681
|
|
|
319
|
-
log
|
|
320
|
-
database: database_name,
|
|
321
|
-
collection: collection_name,
|
|
322
|
-
used_index,
|
|
323
|
-
indexed_field,
|
|
324
|
-
execution_time_ms: execution_time
|
|
325
|
-
});
|
|
326
|
-
|
|
682
|
+
log_no_document_found(log, database_name, collection_name, used_index, indexed_field, execution_time);
|
|
327
683
|
return null;
|
|
328
684
|
} catch (error) {
|
|
329
685
|
log.error('Failed to find document', {
|