@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.
Files changed (49) hide show
  1. package/dist/client/database.js +1 -1
  2. package/dist/client/index.js +1 -1
  3. package/dist/server/cluster/master.js +4 -4
  4. package/dist/server/cluster/worker.js +1 -1
  5. package/dist/server/index.js +1 -1
  6. package/dist/server/lib/auto_index_manager.js +1 -1
  7. package/dist/server/lib/backup_manager.js +1 -1
  8. package/dist/server/lib/index_manager.js +1 -1
  9. package/dist/server/lib/operation_dispatcher.js +1 -1
  10. package/dist/server/lib/operations/admin.js +1 -1
  11. package/dist/server/lib/operations/bulk_write.js +1 -1
  12. package/dist/server/lib/operations/create_index.js +1 -1
  13. package/dist/server/lib/operations/delete_many.js +1 -1
  14. package/dist/server/lib/operations/delete_one.js +1 -1
  15. package/dist/server/lib/operations/find.js +1 -1
  16. package/dist/server/lib/operations/find_one.js +1 -1
  17. package/dist/server/lib/operations/insert_one.js +1 -1
  18. package/dist/server/lib/operations/update_one.js +1 -1
  19. package/dist/server/lib/send_response.js +1 -1
  20. package/dist/server/lib/tcp_protocol.js +1 -1
  21. package/package.json +2 -2
  22. package/src/client/database.js +92 -119
  23. package/src/client/index.js +279 -345
  24. package/src/server/cluster/master.js +265 -156
  25. package/src/server/cluster/worker.js +26 -18
  26. package/src/server/index.js +553 -330
  27. package/src/server/lib/auto_index_manager.js +85 -23
  28. package/src/server/lib/backup_manager.js +117 -70
  29. package/src/server/lib/index_manager.js +63 -25
  30. package/src/server/lib/operation_dispatcher.js +339 -168
  31. package/src/server/lib/operations/admin.js +343 -205
  32. package/src/server/lib/operations/bulk_write.js +458 -194
  33. package/src/server/lib/operations/create_index.js +127 -34
  34. package/src/server/lib/operations/delete_many.js +204 -67
  35. package/src/server/lib/operations/delete_one.js +164 -52
  36. package/src/server/lib/operations/find.js +563 -201
  37. package/src/server/lib/operations/find_one.js +544 -188
  38. package/src/server/lib/operations/insert_one.js +147 -52
  39. package/src/server/lib/operations/update_one.js +334 -93
  40. package/src/server/lib/send_response.js +37 -17
  41. package/src/server/lib/tcp_protocol.js +158 -53
  42. package/tests/server/cluster/master_read_write_operations.test.js +5 -14
  43. package/tests/server/integration/authentication_integration.test.js +18 -10
  44. package/tests/server/integration/backup_integration.test.js +35 -27
  45. package/tests/server/lib/api_key_manager.test.js +88 -32
  46. package/tests/server/lib/development_mode.test.js +2 -2
  47. package/tests/server/lib/operations/admin.test.js +20 -12
  48. package/tests/server/lib/operations/delete_one.test.js +10 -4
  49. 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 (e.g., 'user.profile.name')
18
- * @returns {*} Field value or undefined if path doesn't exist
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.split('.');
65
+ const parts = split_field_path(field_path);
22
66
  let value = document;
23
67
 
24
- for (const part of parts) {
25
- if (value === null || value === undefined) {
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, false otherwise
100
+ * @returns {boolean} True if field exists
39
101
  */
40
102
  const field_exists = (document, field_path) => {
41
- const parts = field_path.split('.');
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 === null || current === undefined || typeof current !== 'object') {
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.hasOwnProperty(parts[i]);
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, false otherwise
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
- const or_match = filter.$or.some(or_condition => matches_filter(document, or_condition));
75
- if (!or_match) return false;
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
- for (const [operator, operand] of Object.entries(value)) {
91
- switch (operator) {
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 !== value) return false;
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
- * Applies projection to a document, including or excluding specified fields.
144
- * Supports both inclusion and exclusion projections with MongoDB-like syntax.
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 with field names and 1/0 or true/false values
147
- * @returns {Object} Projected document with only specified fields
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
- const is_inclusion = Object.values(projection).some(value => value === 1 || value === true);
155
- const projected_document = {};
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
- Object.assign(projected_document, document);
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
- return projected_document;
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 with field names and 1/-1 direction values
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 a_value = a[field];
195
- const b_value = b[field];
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
- * Finds documents in a collection matching the specified filter criteria.
217
- * Supports MongoDB-like querying with automatic index optimization, projection, sorting, and pagination.
218
- * @param {string} database_name - Name of the database
219
- * @param {string} collection_name - Name of the collection to search
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 find = async (database_name, collection_name, filter = {}, options = {}) => {
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
- const { field, operators } = index_info;
253
- const field_filter = filter[field];
254
- indexed_field = field;
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
- const collection_prefix = `${database_name}:${collection_name}:`;
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
- if (limit && limit > 0) {
321
- sorted_documents = sorted_documents.slice(0, limit);
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
- try {
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
- log.info('Find operation completed', {
339
- database: database_name,
340
- collection: collection_name,
341
- documents_found: projected_documents.length,
342
- total_matching: matching_documents.length,
343
- used_index,
344
- indexed_field,
345
- execution_time_ms: execution_time
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) {