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