@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
@@ -9,58 +9,151 @@ import create_logger from '../logger.js';
9
9
  const { create_context_logger } = create_logger('create_index');
10
10
 
11
11
  /**
12
- * Creates or updates an index on a specified field in a collection.
13
- * @param {string} database_name - Name of the database
14
- * @param {string} collection_name - Name of the collection to create index for
15
- * @param {string} field_name - Name of the field to index
16
- * @param {Object} [options={}] - Index creation options
17
- * @param {boolean} [options.unique] - Whether the index should enforce uniqueness
18
- * @param {boolean} [options.sparse] - Whether the index should be sparse (skip null values)
19
- * @returns {Promise<Object>} Index creation result with operation details and message
20
- * @throws {Error} When database, collection name or field name is missing, or index creation fails
12
+ * Validates database name parameter.
13
+ * @param {string} database_name - Database name to validate
14
+ * @throws {Error} When database name is missing
21
15
  */
22
- const create_index_operation = async (database_name, collection_name, field_name, options = {}) => {
23
- const log = create_context_logger();
24
-
16
+ const validate_database_name = (database_name) => {
25
17
  if (!database_name) {
26
18
  throw new Error('Database name is required');
27
19
  }
28
-
20
+ };
21
+
22
+ /**
23
+ * Validates collection name parameter.
24
+ * @param {string} collection_name - Collection name to validate
25
+ * @throws {Error} When collection name is missing
26
+ */
27
+ const validate_collection_name = (collection_name) => {
29
28
  if (!collection_name) {
30
29
  throw new Error('Collection name is required');
31
30
  }
32
-
31
+ };
32
+
33
+ /**
34
+ * Validates field name parameter.
35
+ * @param {string} field_name - Field name to validate
36
+ * @throws {Error} When field name is missing
37
+ */
38
+ const validate_field_name = (field_name) => {
33
39
  if (!field_name) {
34
40
  throw new Error('Field name is required');
35
41
  }
42
+ };
43
+
44
+ /**
45
+ * Validates all create index operation parameters.
46
+ * @param {string} database_name - Database name
47
+ * @param {string} collection_name - Collection name
48
+ * @param {string} field_name - Field name
49
+ */
50
+ const validate_create_index_parameters = (database_name, collection_name, field_name) => {
51
+ validate_database_name(database_name);
52
+ validate_collection_name(collection_name);
53
+ validate_field_name(field_name);
54
+ };
55
+
56
+ /**
57
+ * Determines action verb based on operation type.
58
+ * @param {string} operation_type - Type of operation performed
59
+ * @returns {string} Action verb describing the operation
60
+ */
61
+ const get_action_verb_for_operation = (operation_type) => {
62
+ switch (operation_type) {
63
+ case 'created':
64
+ return 'created';
65
+ case 'updated':
66
+ return 'updated';
67
+ default:
68
+ return 'already exists';
69
+ }
70
+ };
71
+
72
+ /**
73
+ * Creates success message for index operation.
74
+ * @param {string} action_verb - Action verb describing operation
75
+ * @param {string} database_name - Database name
76
+ * @param {string} collection_name - Collection name
77
+ * @param {string} field_name - Field name
78
+ * @returns {string} Success message
79
+ */
80
+ const create_success_message = (action_verb, database_name, collection_name, field_name) => {
81
+ return `Index ${action_verb} on ${database_name}.${collection_name}.${field_name}`;
82
+ };
83
+
84
+ /**
85
+ * Logs successful index operation.
86
+ * @param {Function} log - Logger function
87
+ * @param {string} action_verb - Action verb describing operation
88
+ * @param {string} database_name - Database name
89
+ * @param {string} collection_name - Collection name
90
+ * @param {string} field_name - Field name
91
+ * @param {Object} options - Index options
92
+ * @param {string} operation_type - Type of operation performed
93
+ */
94
+ const log_successful_index_operation = (log, action_verb, database_name, collection_name, field_name, options, operation_type) => {
95
+ log.info(`Index ${action_verb} successfully`, {
96
+ database: database_name,
97
+ collection: collection_name,
98
+ field: field_name,
99
+ options,
100
+ operation_type
101
+ });
102
+ };
103
+
104
+ /**
105
+ * Logs failed index operation.
106
+ * @param {Function} log - Logger function
107
+ * @param {string} database_name - Database name
108
+ * @param {string} collection_name - Collection name
109
+ * @param {string} field_name - Field name
110
+ * @param {Error} error - Error that occurred
111
+ */
112
+ const log_failed_index_operation = (log, database_name, collection_name, field_name, error) => {
113
+ log.error('Failed to create/upsert index', {
114
+ database: database_name,
115
+ collection: collection_name,
116
+ field: field_name,
117
+ error: error.message
118
+ });
119
+ };
120
+
121
+ /**
122
+ * Creates index operation result with message.
123
+ * @param {Object} result - Index creation result
124
+ * @param {string} message - Success message
125
+ * @returns {Object} Enhanced result with message
126
+ */
127
+ const create_index_operation_result = (result, message) => ({
128
+ ...result,
129
+ message
130
+ });
131
+
132
+ /**
133
+ * Creates or updates an index on a specified field in a collection.
134
+ * @param {string} database_name - Name of the database
135
+ * @param {string} collection_name - Name of the collection to create index for
136
+ * @param {string} field_name - Name of the field to index
137
+ * @param {Object} options - Index creation options
138
+ * @returns {Promise<Object>} Index creation result with operation details and message
139
+ */
140
+ const create_index_operation = async (database_name, collection_name, field_name, options = {}) => {
141
+ const log = create_context_logger();
142
+
143
+ validate_create_index_parameters(database_name, collection_name, field_name);
36
144
 
37
145
  try {
38
146
  const result = await create_index(database_name, collection_name, field_name, options);
39
147
 
40
148
  const operation_type = result.operation_type || 'created';
41
- const action_verb = operation_type === 'created' ? 'created' :
42
- operation_type === 'updated' ? 'updated' :
43
- 'already exists';
149
+ const action_verb = get_action_verb_for_operation(operation_type);
150
+ const success_message = create_success_message(action_verb, database_name, collection_name, field_name);
44
151
 
45
- log.info(`Index ${action_verb} successfully`, {
46
- database: database_name,
47
- collection: collection_name,
48
- field: field_name,
49
- options,
50
- operation_type
51
- });
152
+ log_successful_index_operation(log, action_verb, database_name, collection_name, field_name, options, operation_type);
52
153
 
53
- return {
54
- ...result,
55
- message: `Index ${action_verb} on ${database_name}.${collection_name}.${field_name}`
56
- };
154
+ return create_index_operation_result(result, success_message);
57
155
  } catch (error) {
58
- log.error('Failed to create/upsert index', {
59
- database: database_name,
60
- collection: collection_name,
61
- field: field_name,
62
- error: error.message
63
- });
156
+ log_failed_index_operation(log, database_name, collection_name, field_name, error);
64
157
  throw error;
65
158
  }
66
159
  };
@@ -5,6 +5,75 @@ import create_logger from '../logger.js';
5
5
 
6
6
  const { create_context_logger } = create_logger('delete_many');
7
7
 
8
+ /**
9
+ * Validates database name parameter.
10
+ * @param {string} database_name - Database name to validate
11
+ * @throws {Error} When database name is missing
12
+ */
13
+ const validate_database_name = (database_name) => {
14
+ if (!database_name) {
15
+ throw new Error('Database name is required');
16
+ }
17
+ };
18
+
19
+ /**
20
+ * Validates collection name parameter.
21
+ * @param {string} collection_name - Collection name to validate
22
+ * @throws {Error} When collection name is missing
23
+ */
24
+ const validate_collection_name = (collection_name) => {
25
+ if (!collection_name) {
26
+ throw new Error('Collection name is required');
27
+ }
28
+ };
29
+
30
+ /**
31
+ * Validates filter parameter.
32
+ * @param {Object} filter - Filter to validate
33
+ * @throws {Error} When filter is invalid
34
+ */
35
+ const validate_filter = (filter) => {
36
+ if (!filter || typeof filter !== 'object') {
37
+ throw new Error('Filter must be a valid object');
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Validates limit option.
43
+ * @param {number} limit - Limit to validate
44
+ * @throws {Error} When limit is invalid
45
+ */
46
+ const validate_limit = (limit) => {
47
+ if (limit !== undefined && (typeof limit !== 'number' || limit < 0)) {
48
+ throw new Error('Limit must be a non-negative number');
49
+ }
50
+ };
51
+
52
+ /**
53
+ * Validates all delete many operation parameters.
54
+ * @param {string} database_name - Database name
55
+ * @param {string} collection_name - Collection name
56
+ * @param {Object} filter - Filter criteria
57
+ * @param {Object} options - Delete options
58
+ */
59
+ const validate_delete_many_parameters = (database_name, collection_name, filter, options) => {
60
+ validate_database_name(database_name);
61
+ validate_collection_name(collection_name);
62
+ validate_filter(filter);
63
+ validate_limit(options.limit);
64
+ };
65
+
66
+ /**
67
+ * Checks if document field matches filter value.
68
+ * @param {Object} document - Document to check
69
+ * @param {string} field - Field name
70
+ * @param {any} value - Expected value
71
+ * @returns {boolean} True if field matches value
72
+ */
73
+ const field_matches_value = (document, field, value) => {
74
+ return document[field] === value;
75
+ };
76
+
8
77
  /**
9
78
  * Checks if a document matches the given filter criteria (simple equality check).
10
79
  * @param {Object} document - Document to test
@@ -17,7 +86,7 @@ const matches_filter = (document, filter) => {
17
86
  }
18
87
 
19
88
  for (const [field, value] of Object.entries(filter)) {
20
- if (document[field] !== value) {
89
+ if (!field_matches_value(document, field, value)) {
21
90
  return false;
22
91
  }
23
92
  }
@@ -26,81 +95,155 @@ const matches_filter = (document, filter) => {
26
95
  };
27
96
 
28
97
  /**
29
- * Internal implementation of delete_many operation without write queue serialization.
30
- * @param {string} database_name - Name of the database
31
- * @param {string} collection_name - Name of the collection
32
- * @param {Object} filter - Filter criteria to match documents for deletion
33
- * @param {Object} [options={}] - Delete options
34
- * @param {number} [options.limit] - Maximum number of documents to delete
35
- * @returns {Promise<Object>} Delete result with acknowledged and deleted_count
36
- * @throws {Error} When database name, collection name is missing or filter is invalid
98
+ * Attempts to parse document from JSON string.
99
+ * @param {string} value - JSON string
100
+ * @returns {Object|null} Parsed document or null
37
101
  */
38
- const delete_many_internal = async (database_name, collection_name, filter, options = {}) => {
39
- const log = create_context_logger();
40
-
41
- if (!database_name) {
42
- throw new Error('Database name is required');
43
- }
44
-
45
- if (!collection_name) {
46
- throw new Error('Collection name is required');
47
- }
48
-
49
- if (!filter || typeof filter !== 'object') {
50
- throw new Error('Filter must be a valid object');
51
- }
52
-
53
- const { limit } = options;
54
-
55
- if (limit !== undefined && (typeof limit !== 'number' || limit < 0)) {
56
- throw new Error('Limit must be a non-negative number');
102
+ const parse_document_safely = (value) => {
103
+ try {
104
+ return JSON.parse(value);
105
+ } catch (parse_error) {
106
+ return null;
57
107
  }
58
-
59
- const db = get_database();
108
+ };
109
+
110
+ /**
111
+ * Checks if deletion limit has been reached.
112
+ * @param {number} deleted_count - Current deleted count
113
+ * @param {number} limit - Maximum deletion limit
114
+ * @returns {boolean} True if limit reached
115
+ */
116
+ const has_reached_deletion_limit = (deleted_count, limit) => {
117
+ return limit !== undefined && deleted_count >= limit;
118
+ };
119
+
120
+ /**
121
+ * Searches for and deletes matching documents within transaction.
122
+ * @param {Object} db - Database instance
123
+ * @param {string} database_name - Database name
124
+ * @param {string} collection_name - Collection name
125
+ * @param {Object} filter - Filter criteria
126
+ * @param {number} limit - Maximum number of documents to delete
127
+ * @returns {Object} Delete result with count and deleted documents
128
+ */
129
+ const find_and_delete_documents = (db, database_name, collection_name, filter, limit) => {
60
130
  let deleted_count = 0;
61
131
  const deleted_documents = [];
62
132
 
63
- await db.transaction(() => {
64
- const collection_prefix = `${database_name}:${collection_name}:`;
133
+ const collection_prefix = `${database_name}:${collection_name}:`;
134
+ const range = db.getRange({ start: collection_prefix, end: collection_prefix + '\xFF' });
135
+
136
+ for (const { key, value } of range) {
137
+ if (has_reached_deletion_limit(deleted_count, limit)) {
138
+ break;
139
+ }
65
140
 
66
- const range = db.getRange({ start: collection_prefix, end: collection_prefix + '\xFF' });
67
- for (const { key, value } of range) {
68
- // NOTE: Check limit before processing more documents.
69
- if (limit !== undefined && deleted_count >= limit) {
70
- break;
71
- }
72
-
73
- try {
74
- const document = JSON.parse(value);
75
- if (matches_filter(document, filter)) {
76
- db.remove(key);
77
- deleted_documents.push(document);
78
- deleted_count++;
79
- }
80
- } catch (parse_error) {
81
- // Skip documents that can't be parsed
82
- continue;
83
- }
141
+ const document = parse_document_safely(value);
142
+ if (!document) {
143
+ continue;
84
144
  }
85
- });
145
+
146
+ if (matches_filter(document, filter)) {
147
+ db.remove(key);
148
+ deleted_documents.push(document);
149
+ deleted_count++;
150
+ }
151
+ }
86
152
 
87
- // NOTE: Update indexes for all deleted documents.
153
+ return { deleted_count, deleted_documents };
154
+ };
155
+
156
+ /**
157
+ * Updates indexes after document deletions.
158
+ * @param {string} database_name - Database name
159
+ * @param {string} collection_name - Collection name
160
+ * @param {Array<Object>} deleted_documents - Array of deleted documents
161
+ */
162
+ const update_indexes_after_deletions = async (database_name, collection_name, deleted_documents) => {
88
163
  for (const deleted_document of deleted_documents) {
89
164
  await update_indexes_on_delete(database_name, collection_name, deleted_document);
90
165
  }
91
-
166
+ };
167
+
168
+ /**
169
+ * Logs delete many operation completion.
170
+ * @param {Function} log - Logger function
171
+ * @param {string} database_name - Database name
172
+ * @param {string} collection_name - Collection name
173
+ * @param {number} deleted_count - Number of deleted documents
174
+ * @param {number} limit - Deletion limit
175
+ */
176
+ const log_delete_many_completion = (log, database_name, collection_name, deleted_count, limit) => {
92
177
  log.info('Delete many operation completed', {
93
178
  database: database_name,
94
179
  collection: collection_name,
95
180
  deleted_count,
96
181
  limit: limit || 'none'
97
182
  });
183
+ };
184
+
185
+ /**
186
+ * Creates current timestamp string.
187
+ * @returns {string} ISO timestamp string
188
+ */
189
+ const create_current_timestamp = () => {
190
+ return new Date().toISOString();
191
+ };
192
+
193
+ /**
194
+ * Creates delete many operation result.
195
+ * @param {number} deleted_count - Number of deleted documents
196
+ * @returns {Object} Delete result object
197
+ */
198
+ const create_delete_many_result = (deleted_count) => ({
199
+ acknowledged: true,
200
+ deleted_count,
201
+ operation_time: create_current_timestamp()
202
+ });
203
+
204
+ /**
205
+ * Creates write queue operation metadata.
206
+ * @param {string} database_name - Database name
207
+ * @param {string} collection_name - Collection name
208
+ * @param {Object} filter - Filter criteria
209
+ * @param {number} limit - Deletion limit
210
+ * @returns {Object} Operation metadata
211
+ */
212
+ const create_write_queue_metadata = (database_name, collection_name, filter, limit) => ({
213
+ operation: 'delete_many',
214
+ database: database_name,
215
+ collection: collection_name,
216
+ filter_keys: Object.keys(filter || {}),
217
+ limit
218
+ });
219
+
220
+ /**
221
+ * Internal implementation of delete_many operation without write queue serialization.
222
+ * @param {string} database_name - Name of the database
223
+ * @param {string} collection_name - Name of the collection
224
+ * @param {Object} filter - Filter criteria to match documents for deletion
225
+ * @param {Object} options - Delete options
226
+ * @returns {Promise<Object>} Delete result with acknowledged and deleted_count
227
+ */
228
+ const delete_many_internal = async (database_name, collection_name, filter, options = {}) => {
229
+ const log = create_context_logger();
230
+
231
+ validate_delete_many_parameters(database_name, collection_name, filter, options);
232
+
233
+ const { limit } = options;
234
+ const db = get_database();
235
+
236
+ const delete_result = await db.transaction(() => {
237
+ return find_and_delete_documents(db, database_name, collection_name, filter, limit);
238
+ });
98
239
 
99
- return {
100
- acknowledged: true,
101
- deleted_count,
102
- operation_time: new Date().toISOString()
103
- };
240
+ const { deleted_count, deleted_documents } = delete_result;
241
+
242
+ await update_indexes_after_deletions(database_name, collection_name, deleted_documents);
243
+
244
+ log_delete_many_completion(log, database_name, collection_name, deleted_count, limit);
245
+
246
+ return create_delete_many_result(deleted_count);
104
247
  };
105
248
 
106
249
  /**
@@ -108,22 +251,16 @@ const delete_many_internal = async (database_name, collection_name, filter, opti
108
251
  * @param {string} database_name - Name of the database
109
252
  * @param {string} collection_name - Name of the collection
110
253
  * @param {Object} filter - Filter criteria to match documents for deletion
111
- * @param {Object} [options={}] - Delete options
112
- * @param {number} [options.limit] - Maximum number of documents to delete
254
+ * @param {Object} options - Delete options
113
255
  * @returns {Promise<Object>} Delete result with acknowledged, deleted_count, and operation_time
114
256
  */
115
257
  const delete_many = async (database_name, collection_name, filter, options = {}) => {
116
258
  const write_queue = get_write_queue();
259
+ const operation_metadata = create_write_queue_metadata(database_name, collection_name, filter, options.limit);
117
260
 
118
261
  return await write_queue.enqueue_write_operation(
119
262
  () => delete_many_internal(database_name, collection_name, filter, options),
120
- {
121
- operation: 'delete_many',
122
- database: database_name,
123
- collection: collection_name,
124
- filter_keys: Object.keys(filter || {}),
125
- limit: options.limit
126
- }
263
+ operation_metadata
127
264
  );
128
265
  };
129
266