@joystick.js/db-canary 0.0.0-canary.2251 → 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 (97) 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 +552 -319
  37. package/src/server/lib/operations/find_one.js +530 -304
  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/test_data_api_key_1758233848259_cglfjzhou/data.mdb +0 -0
  43. package/test_data_api_key_1758233848259_cglfjzhou/lock.mdb +0 -0
  44. package/test_data_api_key_1758233848502_urlje2utd/data.mdb +0 -0
  45. package/test_data_api_key_1758233848502_urlje2utd/lock.mdb +0 -0
  46. package/test_data_api_key_1758233848738_mtcpfe5ns/data.mdb +0 -0
  47. package/test_data_api_key_1758233848738_mtcpfe5ns/lock.mdb +0 -0
  48. package/test_data_api_key_1758233848856_9g97p6gag/data.mdb +0 -0
  49. package/test_data_api_key_1758233848856_9g97p6gag/lock.mdb +0 -0
  50. package/test_data_api_key_1758233857008_0tl9zzhj8/data.mdb +0 -0
  51. package/test_data_api_key_1758233857008_0tl9zzhj8/lock.mdb +0 -0
  52. package/test_data_api_key_1758233857120_60c2f2uhu/data.mdb +0 -0
  53. package/test_data_api_key_1758233857120_60c2f2uhu/lock.mdb +0 -0
  54. package/test_data_api_key_1758233857232_aw7fkqgd9/data.mdb +0 -0
  55. package/test_data_api_key_1758233857232_aw7fkqgd9/lock.mdb +0 -0
  56. package/test_data_api_key_1758234881285_4aeflubjb/data.mdb +0 -0
  57. package/test_data_api_key_1758234881285_4aeflubjb/lock.mdb +0 -0
  58. package/test_data_api_key_1758234881520_kb0amvtqb/data.mdb +0 -0
  59. package/test_data_api_key_1758234881520_kb0amvtqb/lock.mdb +0 -0
  60. package/test_data_api_key_1758234881756_k04gfv2va/data.mdb +0 -0
  61. package/test_data_api_key_1758234881756_k04gfv2va/lock.mdb +0 -0
  62. package/test_data_api_key_1758234881876_wn90dpo1z/data.mdb +0 -0
  63. package/test_data_api_key_1758234881876_wn90dpo1z/lock.mdb +0 -0
  64. package/test_data_api_key_1758234889461_26xz3dmbr/data.mdb +0 -0
  65. package/test_data_api_key_1758234889461_26xz3dmbr/lock.mdb +0 -0
  66. package/test_data_api_key_1758234889572_uziz7e0p5/data.mdb +0 -0
  67. package/test_data_api_key_1758234889572_uziz7e0p5/lock.mdb +0 -0
  68. package/test_data_api_key_1758234889684_5f9wmposh/data.mdb +0 -0
  69. package/test_data_api_key_1758234889684_5f9wmposh/lock.mdb +0 -0
  70. package/test_data_api_key_1758235657729_prwgm6mxr/data.mdb +0 -0
  71. package/test_data_api_key_1758235657729_prwgm6mxr/lock.mdb +0 -0
  72. package/test_data_api_key_1758235657961_rc2da0dc2/data.mdb +0 -0
  73. package/test_data_api_key_1758235657961_rc2da0dc2/lock.mdb +0 -0
  74. package/test_data_api_key_1758235658193_oqqxm0sny/data.mdb +0 -0
  75. package/test_data_api_key_1758235658193_oqqxm0sny/lock.mdb +0 -0
  76. package/test_data_api_key_1758235658309_vggac1pj6/data.mdb +0 -0
  77. package/test_data_api_key_1758235658309_vggac1pj6/lock.mdb +0 -0
  78. package/test_data_api_key_1758235665968_61ko07dd1/data.mdb +0 -0
  79. package/test_data_api_key_1758235665968_61ko07dd1/lock.mdb +0 -0
  80. package/test_data_api_key_1758235666082_50lrt6sq8/data.mdb +0 -0
  81. package/test_data_api_key_1758235666082_50lrt6sq8/lock.mdb +0 -0
  82. package/test_data_api_key_1758235666194_ykvauwlzh/data.mdb +0 -0
  83. package/test_data_api_key_1758235666194_ykvauwlzh/lock.mdb +0 -0
  84. package/test_data_api_key_1758236187207_9c4paeh09/data.mdb +0 -0
  85. package/test_data_api_key_1758236187207_9c4paeh09/lock.mdb +0 -0
  86. package/test_data_api_key_1758236187441_4n3o3gkkl/data.mdb +0 -0
  87. package/test_data_api_key_1758236187441_4n3o3gkkl/lock.mdb +0 -0
  88. package/test_data_api_key_1758236187672_jt6b21ye0/data.mdb +0 -0
  89. package/test_data_api_key_1758236187672_jt6b21ye0/lock.mdb +0 -0
  90. package/test_data_api_key_1758236187788_oo84fz9u6/data.mdb +0 -0
  91. package/test_data_api_key_1758236187788_oo84fz9u6/lock.mdb +0 -0
  92. package/test_data_api_key_1758236195507_o9zeznwlm/data.mdb +0 -0
  93. package/test_data_api_key_1758236195507_o9zeznwlm/lock.mdb +0 -0
  94. package/test_data_api_key_1758236195619_qsqd60y41/data.mdb +0 -0
  95. package/test_data_api_key_1758236195619_qsqd60y41/lock.mdb +0 -0
  96. package/test_data_api_key_1758236195731_im13iq284/data.mdb +0 -0
  97. package/test_data_api_key_1758236195731_im13iq284/lock.mdb +0 -0
@@ -13,6 +13,60 @@ 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.
18
72
  * Handles nested array object queries like 'reviews.rating' where reviews is an array of objects.
@@ -21,47 +75,37 @@ const { create_context_logger } = create_logger('find_one');
21
75
  * @returns {any} Field value, array of values for nested array queries, or undefined if not found
22
76
  */
23
77
  const get_field_value = (document, field_path) => {
24
- const parts = field_path.split('.');
78
+ const parts = split_field_path(field_path);
25
79
  let value = document;
26
80
 
27
81
  for (let i = 0; i < parts.length; i++) {
28
82
  const part = parts[i];
29
83
 
30
- if (value === null || value === undefined) {
84
+ if (is_null_or_undefined(value)) {
31
85
  return undefined;
32
86
  }
33
87
 
34
88
  value = value[part];
35
89
 
36
- // If we got an array and there are more parts to traverse
37
90
  if (Array.isArray(value) && i < parts.length - 1) {
38
- // Extract the remaining path
39
- const remaining_path = parts.slice(i + 1).join('.');
40
-
41
- // Collect all matching values from array elements
42
- const nested_values = [];
43
- for (let j = 0; j < value.length; j++) {
44
- const item = value[j];
45
- if (typeof item === 'object' && item !== null) {
46
- const nested_value = get_field_value(item, remaining_path);
47
- if (nested_value !== undefined) {
48
- if (Array.isArray(nested_value)) {
49
- nested_values.push(...nested_value);
50
- } else {
51
- nested_values.push(nested_value);
52
- }
53
- }
54
- }
55
- }
56
-
57
- // Return array of values for nested array queries
58
- return nested_values.length > 0 ? nested_values : undefined;
91
+ const remaining_path = get_remaining_path(parts, i);
92
+ return collect_nested_values_from_array(value, remaining_path);
59
93
  }
60
94
  }
61
95
 
62
96
  return value;
63
97
  };
64
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
+
65
109
  /**
66
110
  * Checks if a nested field exists in a document using dot notation.
67
111
  * @param {Object} document - Document to check
@@ -69,16 +113,16 @@ const get_field_value = (document, field_path) => {
69
113
  * @returns {boolean} True if field exists
70
114
  */
71
115
  const field_exists = (document, field_path) => {
72
- const parts = field_path.split('.');
116
+ const parts = split_field_path(field_path);
73
117
  let current = document;
74
118
 
75
119
  for (let i = 0; i < parts.length; i++) {
76
- if (current === null || current === undefined || typeof current !== 'object') {
120
+ if (is_null_or_undefined(current) || typeof current !== 'object') {
77
121
  return false;
78
122
  }
79
123
 
80
124
  if (i === parts.length - 1) {
81
- return current.hasOwnProperty(parts[i]);
125
+ return has_property(current, parts[i]);
82
126
  }
83
127
 
84
128
  current = current[parts[i]];
@@ -87,32 +131,265 @@ const field_exists = (document, field_path) => {
87
131
  return false;
88
132
  };
89
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
+
90
376
  /**
91
377
  * Checks if a document matches the given filter criteria.
92
378
  * Supports MongoDB-like query operators with optimized array field matching.
93
379
  * @param {Object} document - Document to test
94
380
  * @param {Object} filter - Filter criteria with MongoDB-style operators
95
381
  * @returns {boolean} True if document matches filter
96
- * @throws {Error} When unsupported query operator is used
97
382
  */
98
383
  const matches_filter = (document, filter) => {
99
384
  if (!filter || Object.keys(filter).length === 0) {
100
385
  return true;
101
386
  }
102
387
 
103
- // Handle $or operator at the top level
104
388
  if (filter.$or && Array.isArray(filter.$or)) {
105
- const or_conditions = filter.$or;
106
- let or_match = false;
107
- for (let i = 0; i < or_conditions.length; i++) {
108
- if (matches_filter(document, or_conditions[i])) {
109
- or_match = true;
110
- break;
111
- }
389
+ if (!handle_or_operator(document, filter.$or)) {
390
+ return false;
112
391
  }
113
- if (!or_match) return false;
114
392
 
115
- // Continue checking other conditions (if any) outside of $or
116
393
  const remaining_filter = { ...filter };
117
394
  delete remaining_filter.$or;
118
395
  if (Object.keys(remaining_filter).length > 0) {
@@ -125,143 +402,11 @@ const matches_filter = (document, filter) => {
125
402
  const field_value = get_field_value(document, field);
126
403
 
127
404
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
128
- for (const [operator, operand] of Object.entries(value)) {
129
- switch (operator) {
130
- case '$eq':
131
- // Handle array field matching for $eq
132
- if (Array.isArray(field_value)) {
133
- if (!field_value.includes(operand)) return false;
134
- } else if (field_value !== operand) {
135
- return false;
136
- }
137
- break;
138
- case '$ne':
139
- // Handle array field matching for $ne
140
- if (Array.isArray(field_value)) {
141
- if (field_value.includes(operand)) return false;
142
- } else if (field_value === operand) {
143
- return false;
144
- }
145
- break;
146
- case '$gt':
147
- if (Array.isArray(field_value)) {
148
- let found = false;
149
- for (let i = 0; i < field_value.length; i++) {
150
- if (field_value[i] > operand) {
151
- found = true;
152
- break;
153
- }
154
- }
155
- if (!found) return false;
156
- } else if (field_value <= operand) {
157
- return false;
158
- }
159
- break;
160
- case '$gte':
161
- if (Array.isArray(field_value)) {
162
- let found = false;
163
- for (let i = 0; i < field_value.length; i++) {
164
- if (field_value[i] >= operand) {
165
- found = true;
166
- break;
167
- }
168
- }
169
- if (!found) return false;
170
- } else if (field_value < operand) {
171
- return false;
172
- }
173
- break;
174
- case '$lt':
175
- if (Array.isArray(field_value)) {
176
- let found = false;
177
- for (let i = 0; i < field_value.length; i++) {
178
- if (field_value[i] < operand) {
179
- found = true;
180
- break;
181
- }
182
- }
183
- if (!found) return false;
184
- } else if (field_value >= operand) {
185
- return false;
186
- }
187
- break;
188
- case '$lte':
189
- if (Array.isArray(field_value)) {
190
- let found = false;
191
- for (let i = 0; i < field_value.length; i++) {
192
- if (field_value[i] <= operand) {
193
- found = true;
194
- break;
195
- }
196
- }
197
- if (!found) return false;
198
- } else if (field_value > operand) {
199
- return false;
200
- }
201
- break;
202
- case '$in':
203
- if (!Array.isArray(operand)) return false;
204
- if (Array.isArray(field_value)) {
205
- // Check if any array element is in the operand array
206
- let found = false;
207
- for (let i = 0; i < field_value.length; i++) {
208
- if (operand.includes(field_value[i])) {
209
- found = true;
210
- break;
211
- }
212
- }
213
- if (!found) return false;
214
- } else if (!operand.includes(field_value)) {
215
- return false;
216
- }
217
- break;
218
- case '$nin':
219
- if (!Array.isArray(operand)) return false;
220
- if (Array.isArray(field_value)) {
221
- // Check if any array element is in the operand array
222
- for (let i = 0; i < field_value.length; i++) {
223
- if (operand.includes(field_value[i])) {
224
- return false;
225
- }
226
- }
227
- } else if (operand.includes(field_value)) {
228
- return false;
229
- }
230
- break;
231
- case '$exists':
232
- const exists = field_exists(document, field);
233
- if (operand && !exists) return false;
234
- if (!operand && exists) return false;
235
- break;
236
- case '$regex':
237
- // Handle $options parameter for regex flags
238
- const regex_options = value.$options || '';
239
- const regex = new RegExp(operand, regex_options);
240
- if (Array.isArray(field_value)) {
241
- let found = false;
242
- for (let i = 0; i < field_value.length; i++) {
243
- if (typeof field_value[i] === 'string' && regex.test(field_value[i])) {
244
- found = true;
245
- break;
246
- }
247
- }
248
- if (!found) return false;
249
- } else if (!regex.test(field_value)) {
250
- return false;
251
- }
252
- break;
253
- case '$options':
254
- // $options is handled as part of $regex, skip it here
255
- break;
256
- default:
257
- throw new Error(`Unsupported query operator: ${operator}`);
258
- }
405
+ if (!process_query_operators(document, field, field_value, value)) {
406
+ return false;
259
407
  }
260
408
  } else {
261
- // Direct field matching with array support
262
- if (Array.isArray(field_value)) {
263
- if (!field_value.includes(value)) return false;
264
- } else if (field_value !== value) {
409
+ if (!handle_equality_operator(field_value, value)) {
265
410
  return false;
266
411
  }
267
412
  }
@@ -270,10 +415,57 @@ const matches_filter = (document, filter) => {
270
415
  return true;
271
416
  };
272
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
+
273
465
  /**
274
466
  * Applies projection to a document, including or excluding specified fields.
275
467
  * @param {Object} document - Document to project
276
- * @param {Object} projection - Projection specification (1/true for inclusion, 0/false for exclusion)
468
+ * @param {Object} projection - Projection specification
277
469
  * @returns {Object} Projected document
278
470
  */
279
471
  const apply_projection = (document, projection) => {
@@ -281,56 +473,183 @@ const apply_projection = (document, projection) => {
281
473
  return document;
282
474
  }
283
475
 
284
- const is_inclusion = Object.values(projection).some(value => value === 1 || value === true);
285
- const projected_document = {};
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
+ }
286
493
 
287
- if (is_inclusion) {
288
- projected_document._id = document._id;
289
-
290
- for (const [field, include] of Object.entries(projection)) {
291
- if (field === '_id' && (include === 0 || include === false)) {
292
- delete projected_document._id;
293
- } else if (include === 1 || include === true) {
294
- projected_document[field] = document[field];
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;
295
534
  }
296
535
  }
297
- } else {
298
- Object.assign(projected_document, document);
299
-
300
- for (const [field, exclude] of Object.entries(projection)) {
301
- if (exclude === 0 || exclude === false) {
302
- delete projected_document[field];
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
+ }
303
550
  }
304
551
  }
305
552
  }
306
553
 
307
- return projected_document;
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
+ });
308
636
  };
309
637
 
310
638
  /**
311
639
  * Finds a single document in a collection matching the filter criteria.
312
640
  * @param {string} database_name - Name of the database
313
641
  * @param {string} collection_name - Name of the collection
314
- * @param {Object} [filter={}] - Filter criteria
315
- * @param {Object} [options={}] - Query options
316
- * @param {Object} [options.projection] - Field projection specification
317
- * @param {Object} [options.sort] - Sort specification (currently unused)
642
+ * @param {Object} filter - Filter criteria
643
+ * @param {Object} options - Query options
318
644
  * @returns {Promise<Object|null>} Found document or null if not found
319
- * @throws {Error} When database or collection name is missing or query fails
320
645
  */
321
646
  const find_one = async (database_name, collection_name, filter = {}, options = {}) => {
322
647
  const log = create_context_logger();
323
648
 
324
- if (!database_name) {
325
- throw new Error('Database name is required');
326
- }
327
-
328
- if (!collection_name) {
329
- throw new Error('Collection name is required');
330
- }
649
+ validate_find_parameters(database_name, collection_name);
331
650
 
332
651
  const db = get_database();
333
- const { projection, sort } = options;
652
+ const { projection } = options;
334
653
  const start_time = Date.now();
335
654
 
336
655
  try {
@@ -341,119 +660,26 @@ const find_one = async (database_name, collection_name, filter = {}, options = {
341
660
  const index_info = can_use_index(database_name, collection_name, filter);
342
661
 
343
662
  if (index_info) {
344
- const { field, operators } = index_info;
345
- const field_filter = filter[field];
346
- indexed_field = field;
347
-
348
- if (typeof field_filter === 'object' && field_filter !== null && !Array.isArray(field_filter)) {
349
- for (const operator of operators) {
350
- if (field_filter[operator] !== undefined) {
351
- const document_ids = find_documents_by_index(database_name, collection_name, field, operator, field_filter[operator]);
352
-
353
- if (document_ids && document_ids.length > 0) {
354
- used_index = true;
355
- record_index_usage(database_name, collection_name, field);
356
-
357
- for (const document_id of document_ids) {
358
- const collection_key = build_collection_key(database_name, collection_name, document_id);
359
- const value = db.get(collection_key);
360
-
361
- if (value) {
362
- try {
363
- const document = JSON.parse(value);
364
- if (matches_filter(document, filter)) {
365
- found_document = document;
366
- break;
367
- }
368
- } catch (parse_error) {
369
- // Skip documents that can't be parsed
370
- continue;
371
- }
372
- }
373
- }
374
- break;
375
- }
376
- }
377
- }
378
- } else if (operators.includes('eq')) {
379
- const document_ids = find_documents_by_index(database_name, collection_name, field, 'eq', field_filter);
380
-
381
- if (document_ids && document_ids.length > 0) {
382
- used_index = true;
383
- record_index_usage(database_name, collection_name, field);
384
-
385
- for (const document_id of document_ids) {
386
- const collection_key = build_collection_key(database_name, collection_name, document_id);
387
- const value = db.get(collection_key);
388
-
389
- if (value) {
390
- try {
391
- const document = JSON.parse(value);
392
- if (matches_filter(document, filter)) {
393
- found_document = document;
394
- break;
395
- }
396
- } catch (parse_error) {
397
- // Skip documents that can't be parsed
398
- continue;
399
- }
400
- }
401
- }
402
- }
403
- }
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;
404
666
  }
405
667
 
406
668
  if (!used_index) {
407
- const collection_prefix = `${database_name}:${collection_name}:`;
408
- const range = db.getRange({ start: collection_prefix, end: collection_prefix + '\xFF' });
409
-
410
- for (const { key, value } of range) {
411
- try {
412
- const document = JSON.parse(value);
413
- if (matches_filter(document, filter)) {
414
- found_document = document;
415
- break;
416
- }
417
- } catch (parse_error) {
418
- // Skip documents that can't be parsed
419
- continue;
420
- }
421
- }
669
+ found_document = search_using_full_scan(db, database_name, collection_name, filter);
422
670
  }
423
671
 
424
672
  const execution_time = Date.now() - start_time;
425
673
 
426
- try {
427
- record_query(collection_name, filter, execution_time, used_index, indexed_field);
428
- } catch (auto_index_error) {
429
- log.warn('Failed to record query for auto-indexing', {
430
- error: auto_index_error.message
431
- });
432
- }
674
+ record_query_for_auto_indexing(log, collection_name, filter, execution_time, used_index, indexed_field);
433
675
 
434
676
  if (found_document) {
435
677
  const projected_document = apply_projection(found_document, projection);
436
-
437
- log.info('Document found', {
438
- database: database_name,
439
- collection: collection_name,
440
- document_id: found_document._id,
441
- used_index,
442
- indexed_field,
443
- execution_time_ms: execution_time
444
- });
445
-
678
+ log_document_found(log, database_name, collection_name, found_document._id, used_index, indexed_field, execution_time);
446
679
  return projected_document;
447
680
  }
448
681
 
449
- log.info('No document found', {
450
- database: database_name,
451
- collection: collection_name,
452
- used_index,
453
- indexed_field,
454
- execution_time_ms: execution_time
455
- });
456
-
682
+ log_no_document_found(log, database_name, collection_name, used_index, indexed_field, execution_time);
457
683
  return null;
458
684
  } catch (error) {
459
685
  log.error('Failed to find document', {