alchemymvc 1.4.0-alpha.9 → 1.4.1

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 (76) hide show
  1. package/lib/app/behaviour/revision_behaviour.js +1 -1
  2. package/lib/app/behaviour/sluggable_behaviour.js +74 -3
  3. package/lib/app/conduit/electron_conduit.js +1 -1
  4. package/lib/app/conduit/socket_conduit.js +62 -8
  5. package/lib/app/controller/alchemy_info_controller.js +2 -2
  6. package/lib/app/datasource/mongo_datasource.js +103 -107
  7. package/lib/app/element/al_time.js +126 -0
  8. package/lib/app/helper/cron.js +2 -2
  9. package/lib/app/helper/enum_values.js +9 -0
  10. package/lib/app/helper/router_helper.js +37 -5
  11. package/lib/app/helper/socket_helper.js +71 -14
  12. package/lib/app/helper/syncable.js +253 -30
  13. package/lib/app/helper_datasource/00-nosql_datasource.js +410 -54
  14. package/lib/app/helper_datasource/05-fallback_datasource.js +7 -2
  15. package/lib/app/helper_datasource/idb_datasource.js +82 -64
  16. package/lib/app/helper_datasource/indexed_db.js +41 -33
  17. package/lib/app/helper_datasource/read_operational_context.js +0 -17
  18. package/lib/app/helper_datasource/remote_datasource.js +1 -1
  19. package/lib/app/helper_field/00-objectid_field.js +16 -0
  20. package/lib/app/helper_field/11-date_field.js +13 -0
  21. package/lib/app/helper_field/datetime_field.js +13 -0
  22. package/lib/app/helper_field/enum_field.js +27 -0
  23. package/lib/app/helper_field/geopoint_field.js +61 -0
  24. package/lib/app/helper_field/html_field.js +14 -1
  25. package/lib/app/helper_field/integer_field.js +14 -0
  26. package/lib/app/helper_field/local_date_field.js +13 -0
  27. package/lib/app/helper_field/local_date_time_field.js +13 -0
  28. package/lib/app/helper_field/local_time_field.js +13 -0
  29. package/lib/app/helper_field/password_field.js +28 -2
  30. package/lib/app/helper_field/schema_field.js +88 -4
  31. package/lib/app/helper_field/url_field.js +22 -0
  32. package/lib/app/helper_model/00-base_criteria.js +81 -6
  33. package/lib/app/helper_model/05-criteria_expressions.js +53 -11
  34. package/lib/app/helper_model/10-model_criteria.js +82 -22
  35. package/lib/app/helper_model/document.js +23 -9
  36. package/lib/app/helper_model/field_config.js +9 -2
  37. package/lib/app/helper_model/model.js +15 -11
  38. package/lib/app/model/system_task_history_model.js +11 -1
  39. package/lib/class/conduit.js +116 -13
  40. package/lib/class/controller.js +1 -0
  41. package/lib/class/datasource.js +30 -2
  42. package/lib/class/document.js +40 -12
  43. package/lib/class/field.js +170 -7
  44. package/lib/class/import_stream_parser.js +299 -0
  45. package/lib/class/inode_file.js +2 -2
  46. package/lib/class/migration.js +5 -2
  47. package/lib/class/model.js +22 -151
  48. package/lib/class/operational_context.js +37 -0
  49. package/lib/class/plugin.js +32 -3
  50. package/lib/class/route.js +34 -3
  51. package/lib/class/router.js +38 -34
  52. package/lib/class/schema.js +1 -1
  53. package/lib/class/schema_client.js +172 -14
  54. package/lib/class/session.js +1 -1
  55. package/lib/class/sitemap.js +2 -2
  56. package/lib/class/task.js +30 -1
  57. package/lib/class/task_service.js +83 -14
  58. package/lib/core/alchemy.js +188 -166
  59. package/lib/core/alchemy_functions.js +7 -11
  60. package/lib/core/alchemy_load_functions.js +101 -10
  61. package/lib/core/base.js +2 -2
  62. package/lib/core/client_alchemy.js +9 -1
  63. package/lib/core/middleware.js +83 -25
  64. package/lib/core/setting.js +86 -14
  65. package/lib/scripts/create_constants.js +5 -1
  66. package/lib/scripts/create_settings.js +44 -0
  67. package/lib/scripts/setup_ai_devmode.js +324 -0
  68. package/lib/scripts/setup_devwatch.js +0 -0
  69. package/lib/stages/00-load_core.js +9 -3
  70. package/lib/stages/50-routes.js +6 -1
  71. package/lib/stages/90-server.js +0 -1
  72. package/lib/testing/browser.js +1164 -0
  73. package/lib/testing/harness.js +840 -0
  74. package/package.json +26 -19
  75. package/testing/browser.js +27 -0
  76. package/testing.js +37 -0
@@ -320,7 +320,7 @@ Revision.setMethod(function afterSave(record, options, created) {
320
320
  [that.revision_model.model_name] : revision_data
321
321
  };
322
322
 
323
- // Save the data
323
+ // Save the data (but do not wait for it)
324
324
  that.revision_model.save(revision_data, {allowFields: true});
325
325
  }
326
326
  }
@@ -141,7 +141,7 @@ Sluggable.setStatic(function attached(schema, new_options) {
141
141
  *
142
142
  * @author Jelle De Loecker <jelle@elevenways.be>
143
143
  * @since 0.1.0
144
- * @version 1.0.6
144
+ * @version 1.4.1
145
145
  *
146
146
  * @param {Object} data The data that is to be saved
147
147
  * @param {Object} options Behaviour options
@@ -172,7 +172,7 @@ Sluggable.setMethod(async function beforeSave(data, options, creating) {
172
172
  if (!creating) {
173
173
  old_record = await this.model.findById(data._id);
174
174
 
175
- if (old_value) {
175
+ if (old_record) {
176
176
  old_value = old_record[that.target_field.name];
177
177
  }
178
178
  }
@@ -282,15 +282,86 @@ Sluggable.setMethod(function createSlug(record, new_value, callback) {
282
282
 
283
283
  return Function.parallel(tasks, callback);
284
284
  } else {
285
+ let existing_slug = record[that.target_field.name];
286
+
285
287
  // Only generate a new slug if it doesn't exist yet
286
- if (!record[that.target_field.name]) {
288
+ if (!existing_slug) {
287
289
  return that.generateSlug(record[that.source_field.name], false, record, callback);
288
290
  }
291
+
292
+ // Check if the existing slug is a duplicate (belongs to another record)
293
+ return that.checkSlugIsDuplicate(existing_slug, false, record, function checked(err, is_duplicate) {
294
+ if (err) {
295
+ return callback(err);
296
+ }
297
+
298
+ // If the slug is a duplicate, regenerate from the source field
299
+ if (is_duplicate) {
300
+ return that.generateSlug(record[that.source_field.name], false, record, callback);
301
+ }
302
+
303
+ // Slug is not a duplicate, keep it
304
+ callback(null, existing_slug);
305
+ });
289
306
  }
290
307
 
291
308
  callback();
292
309
  });
293
310
 
311
+ /**
312
+ * Check if a slug already exists (belongs to another record)
313
+ *
314
+ * @author Jelle De Loecker <jelle@elevenways.be>
315
+ * @since 1.4.0
316
+ * @version 1.4.0
317
+ *
318
+ * @param {string} slug
319
+ * @param {string} key The translation key (or false)
320
+ * @param {Document} record
321
+ * @param {Function} callback
322
+ */
323
+ Sluggable.setMethod(function checkSlugIsDuplicate(slug, key, record, callback) {
324
+
325
+ let that = this,
326
+ for_record_id = record?._id,
327
+ model = Model.get(this.model.name),
328
+ path = this.target_field.name;
329
+
330
+ if (key) {
331
+ path += '.' + key;
332
+ }
333
+
334
+ let conditions = {};
335
+ conditions[path] = slug;
336
+
337
+ // Also include unique_modifier_fields in the check
338
+ if (this.unique_modifier_fields.length && record) {
339
+ for (let i = 0; i < this.unique_modifier_fields.length; i++) {
340
+ let field = this.unique_modifier_fields[i];
341
+ conditions[field] = record[field];
342
+ }
343
+ }
344
+
345
+ model.find('first', {conditions: conditions}, function gotFirst(err, found_item) {
346
+ if (err) {
347
+ return callback(err);
348
+ }
349
+
350
+ // If no item found, it's not a duplicate
351
+ if (!found_item) {
352
+ return callback(null, false);
353
+ }
354
+
355
+ // If the found item is the same as the current record, it's not a duplicate
356
+ if (for_record_id && String(for_record_id) == String(found_item._id)) {
357
+ return callback(null, false);
358
+ }
359
+
360
+ // It's a duplicate
361
+ callback(null, true);
362
+ });
363
+ });
364
+
294
365
  /**
295
366
  * Actually generate the slug from the given string,
296
367
  * and look for existing slugs in the path
@@ -80,7 +80,7 @@ ElectronConduit.setMethod(function prepareViewRender() {
80
80
  });
81
81
 
82
82
  ElectronConduit.setMethod(function setHeader(name, val) {
83
- console.log('Setting header', name, val);
83
+ // Headers are not used in Electron conduit
84
84
  });
85
85
 
86
86
  /**
@@ -6,7 +6,7 @@ var iostream = alchemy.use('socket.io-stream'),
6
6
  *
7
7
  * @author Jelle De Loecker <jelle@elevenways.be>
8
8
  * @since 0.2.0
9
- * @version 1.3.10
9
+ * @version 1.4.0
10
10
  *
11
11
  * @param {Socker} socket
12
12
  * @param {Object} announcement
@@ -25,6 +25,10 @@ var SocketConduit = Function.inherits('Alchemy.Conduit', function Socket(socket,
25
25
  // Store the announcement data
26
26
  this.announcement = announcement;
27
27
 
28
+ if (!this.canCreateSocketConnection()) {
29
+ return;
30
+ }
31
+
28
32
  // Detect node clients
29
33
  if (this.headers['user-agent'] == 'node-XMLHttpRequest') {
30
34
  this.isNodeClient = true;
@@ -84,8 +88,8 @@ var SocketConduit = Function.inherits('Alchemy.Conduit', function Socket(socket,
84
88
 
85
89
  // Listen for responses on the stream socket
86
90
  this.stream.on('response', function onStreamResponse(stream, data) {
87
- packet.stream = stream;
88
- that.onPayload(packet);
91
+ data.stream = stream;
92
+ that.onPayload(data);
89
93
  });
90
94
 
91
95
  // Listen to data subscriptions
@@ -145,6 +149,22 @@ SocketConduit.setProperty(function is_connected() {
145
149
  return this.socket?.connected || false;
146
150
  });
147
151
 
152
+ /**
153
+ * Is this client allowed to create a socket connection?
154
+ *
155
+ * @author Jelle De Loecker <jelle@elevenways.be>
156
+ * @since 1.4.0
157
+ * @version 1.4.0
158
+ */
159
+ SocketConduit.setMethod(function canCreateSocketConnection() {
160
+
161
+ if (this.isCrawler()) {
162
+ return false;
163
+ }
164
+
165
+ return true;
166
+ });
167
+
148
168
  /**
149
169
  * Parse the request, get information from the url
150
170
  *
@@ -166,7 +186,7 @@ SocketConduit.setMethod(function parseRequest() {
166
186
  *
167
187
  * @param {Object} data
168
188
  */
169
- SocketConduit.setMethod(function parseAnnouncement() {
189
+ SocketConduit.setMethod(async function parseAnnouncement() {
170
190
 
171
191
  var connections,
172
192
  data = this.announcement;
@@ -179,6 +199,10 @@ SocketConduit.setMethod(function parseAnnouncement() {
179
199
  // Register the connection in the user's session
180
200
  this.getSession().registerConnection(this);
181
201
 
202
+ // Allow plugins to restore session state (e.g., persistent login cookies)
203
+ // This is critical after server restarts when the session exists but has no user data
204
+ await alchemy.emit('restoring_websocket_session', this);
205
+
182
206
  // Tell the client we're ready
183
207
  this.websocket.emit('ready');
184
208
 
@@ -192,7 +216,7 @@ SocketConduit.setMethod(function parseAnnouncement() {
192
216
  *
193
217
  * @author Jelle De Loecker <jelle@elevenways.be>
194
218
  * @since 0.2.0
195
- * @version 0.4.0
219
+ * @version 1.4.0
196
220
  *
197
221
  * @param {Object} packet
198
222
  */
@@ -237,9 +261,39 @@ SocketConduit.setMethod(function onLinkup(packet) {
237
261
  router = Router;
238
262
  }
239
263
 
264
+ // Helper to send permission denied error and clean up
265
+ const send_permission_denied = (required_permission) => {
266
+ let error_linkup = new Linkup(this, type, id, packet.data);
267
+ error_linkup.submit('error', {
268
+ code: 'PERMISSION_DENIED',
269
+ message: 'Permission denied',
270
+ required_permission: required_permission
271
+ });
272
+ error_linkup.destroy();
273
+ };
274
+
240
275
  // See if any socket routes have been set
241
276
  if (router.linkupRoutes[type]) {
242
- fnc = router.linkupRoutes[type];
277
+ let route_config = router.linkupRoutes[type];
278
+
279
+ // Support both old format (just fnc) and new format ({fnc, permission})
280
+ if (typeof route_config === 'function' || typeof route_config === 'string') {
281
+ fnc = route_config;
282
+ } else {
283
+ fnc = route_config.fnc;
284
+
285
+ // Check the linkup route's permission
286
+ if (route_config.permission && !this.hasPermission(route_config.permission)) {
287
+ send_permission_denied(route_config.permission);
288
+ return;
289
+ }
290
+ }
291
+
292
+ // Check the router section's permissions
293
+ if (!router.checkPermission(this)) {
294
+ send_permission_denied(router.permissions);
295
+ return;
296
+ }
243
297
 
244
298
  // Create the new linkup instance
245
299
  linkup = new Linkup(this, type, id, packet.data);
@@ -335,7 +389,7 @@ SocketConduit.setMethod(function onPayload(packet) {
335
389
  let test = linkup.simpleListeners.get(packet.type);
336
390
 
337
391
  if (!test) {
338
- console.error('Linkup', linkup, 'has no listener for', packet.type);
392
+ log.warn('Linkup has no listener for', packet.type);
339
393
  }
340
394
 
341
395
  if (packet.stream) {
@@ -679,6 +733,6 @@ Linkup.setMethod(function createStream() {
679
733
  * @param {Function} callback
680
734
  */
681
735
  Linkup.setMethod(function error(err, callback) {
682
- console.log('Error:', err);
736
+ log.error('Linkup error:', err);
683
737
  this.submit('error', {stack: err.stack, message: err.message}, callback);
684
738
  });
@@ -149,8 +149,8 @@ Info.setAction(function postponed(conduit, id) {
149
149
  *
150
150
  * @author Jelle De Loecker <jelle@elevenways.be>
151
151
  * @since 1.3.10
152
- * @version 1.3.10
152
+ * @version 1.4.0
153
153
  */
154
154
  Info.setAction(function syncable(conduit, linkup, config) {
155
- Classes.Alchemy.Syncable.handleLink(conduit, linkup, config);
155
+ Classes.Alchemy.Syncable.Syncable.handleLink(conduit, linkup, config);
156
156
  });
@@ -3,6 +3,7 @@ const mongo = alchemy.use('mongodb'),
3
3
 
4
4
  const CONNECTION = Symbol('connection'),
5
5
  CONNECTION_ERROR = Symbol('connection_error'),
6
+ CONNECTION_ERROR_TIME = Symbol('connection_error_time'),
6
7
  MONGO_CLIENT = Symbol('mongo_client');
7
8
 
8
9
  /**
@@ -10,7 +11,7 @@ const CONNECTION = Symbol('connection'),
10
11
  *
11
12
  * @author Jelle De Loecker <jelle@elevenways.be>
12
13
  * @since 0.2.0
13
- * @version 1.3.16
14
+ * @version 1.4.1
14
15
  */
15
16
  const Mongo = Function.inherits('Alchemy.Datasource.Nosql', function Mongo(name, _options) {
16
17
 
@@ -20,6 +21,9 @@ const Mongo = Function.inherits('Alchemy.Datasource.Nosql', function Mongo(name,
20
21
  // Possible connection error
21
22
  this[CONNECTION_ERROR] = null;
22
23
 
24
+ // Time of the connection error (for retry logic)
25
+ this[CONNECTION_ERROR_TIME] = null;
26
+
23
27
  // The actual DB connection
24
28
  this[CONNECTION] = null;
25
29
 
@@ -84,7 +88,7 @@ Mongo.setSupport('querying_associations', true);
84
88
  *
85
89
  * @return {Object}
86
90
  */
87
- Mongo.setProperty('allowed_find_options', [
91
+ Mongo.setProperty('allowed_find_options', new Set([
88
92
  'limit',
89
93
  'sort',
90
94
  'projection',
@@ -111,7 +115,7 @@ Mongo.setProperty('allowed_find_options', [
111
115
  'maxTimeMS',
112
116
  'collation',
113
117
  'session'
114
- ]);
118
+ ]));
115
119
 
116
120
  /**
117
121
  * Convert the given value to a BigInt
@@ -230,7 +234,7 @@ Mongo.setMethod(function normalizeFindOptions(options) {
230
234
  }
231
235
 
232
236
  for (key in options) {
233
- if (this.allowed_find_options.indexOf(key) == -1) {
237
+ if (!this.allowed_find_options.has(key)) {
234
238
  continue;
235
239
  }
236
240
 
@@ -245,7 +249,7 @@ Mongo.setMethod(function normalizeFindOptions(options) {
245
249
  *
246
250
  * @author Jelle De Loecker <jelle@elevenways.be>
247
251
  * @since 0.2.0
248
- * @version 1.4.0
252
+ * @version 1.4.1
249
253
  *
250
254
  * @param {Pledge}
251
255
  */
@@ -256,7 +260,18 @@ Mongo.setMethod(function connect() {
256
260
  }
257
261
 
258
262
  if (this[CONNECTION_ERROR]) {
259
- throw this[CONNECTION_ERROR];
263
+ // Allow retry after 5 seconds (configurable via options.retry_delay)
264
+ let retry_delay = this.options.retry_delay ?? 5000;
265
+ let error_age = Date.now() - this[CONNECTION_ERROR_TIME];
266
+
267
+ if (error_age < retry_delay) {
268
+ throw this[CONNECTION_ERROR];
269
+ }
270
+
271
+ // Clear the cached error to allow retry
272
+ this[CONNECTION_ERROR] = null;
273
+ this[CONNECTION_ERROR_TIME] = null;
274
+ log.info('Retrying connection to Mongo datasource', this.name);
260
275
  }
261
276
 
262
277
  let pledge = this[CONNECTION] = new Swift();
@@ -268,6 +283,7 @@ Mongo.setMethod(function connect() {
268
283
 
269
284
  if (err) {
270
285
  this[CONNECTION_ERROR] = err;
286
+ this[CONNECTION_ERROR_TIME] = Date.now();
271
287
  alchemy.printLog(alchemy.SEVERE, 'Could not create connection to Mongo server', {err: err});
272
288
  return pledge.reject(err);
273
289
  } else {
@@ -354,90 +370,69 @@ Mongo.setMethod(function _read(context) {
354
370
  // Sorting should happen in the pipeline
355
371
  if (options.sort && options.sort.length) {
356
372
  let sort_object = {};
357
-
373
+
358
374
  for (let entry of options.sort) {
359
- sort_object[entry[0]] = entry[1];
375
+ let field_name = entry[0];
376
+ let assoc_name = entry[2];
377
+
378
+ // If there's an association, prefix the field with the association alias
379
+ if (assoc_name) {
380
+ field_name = assoc_name + '.' + field_name;
381
+ }
382
+
383
+ sort_object[field_name] = entry[1];
360
384
  }
361
-
362
- compiled.pipeline.unshift({$sort: sort_object});
385
+
386
+ // Add $sort at the END of the pipeline (after $lookup stages)
387
+ // so that we can sort by fields from associated models
388
+ compiled.pipeline.push({$sort: sort_object});
363
389
  }
364
-
365
- // Skipping also happens in the pipeline
390
+
391
+ // Use $facet to combine count and data retrieval in a single query
392
+ // This avoids running the expensive $lookup stages twice
393
+ // Note: $skip and $limit go INSIDE the data facet so count reflects total matches
394
+ let data_pipeline = [];
395
+
366
396
  if (options.skip) {
367
- compiled.pipeline.push({$skip: options.skip});
397
+ data_pipeline.push({$skip: options.skip});
368
398
  }
369
-
370
- let aggregate_options = {};
371
-
372
- Function.parallel({
373
- available: function getAvailable(next) {
374
-
375
- if (criteria.options.available === false) {
376
- return next(null, null);
377
- }
378
-
379
- let pipeline = JSON.clone(compiled.pipeline),
380
- cloned_options = JSON.clone(aggregate_options);
381
-
382
- pipeline.push({$count: 'available'});
383
-
384
- // Expensive aggregate just to get the available count...
385
- Pledge.done(collection.aggregate(pipeline, cloned_options), function gotAggregate(err, cursor) {
386
-
387
- if (err) {
388
- return next(err);
389
- }
390
-
391
- Pledge.done(cursor.toArray(), function gotAvailableArray(err, items) {
392
-
393
- if (err) {
394
- return next(err);
395
- }
396
-
397
- if (!items || !items.length) {
398
- return next(null, null);
399
- }
400
-
401
- let available = items[0].available;
402
-
403
- if (options.skip) {
404
- available += options.skip;
405
- }
406
-
407
- return next(null, available);
408
- });
409
- });
410
- },
411
- rows: function getRows(next) {
412
-
413
- let pipeline = JSON.clone(compiled.pipeline);
414
-
415
- // Limits also have to be set in the pipeline now
416
- // (We have to do it here, so the `available` count is correct)
417
- if (options.limit) {
418
- pipeline.push({$limit: options.limit});
419
- }
420
-
421
- Pledge.done(collection.aggregate(pipeline, aggregate_options), function gotAggregate(err, cursor) {
422
-
423
- if (err) {
424
- return next(err);
425
- }
426
-
427
- Pledge.done(cursor.toArray(), next);
428
- });
399
+
400
+ if (options.limit) {
401
+ data_pipeline.push({$limit: options.limit});
402
+ }
403
+
404
+ let facet_stage = {
405
+ $facet: {
406
+ data: data_pipeline,
429
407
  }
430
- }, function done(err, data) {
431
-
432
- if (err) {
433
- return pledge.reject(err);
408
+ };
409
+
410
+ // Only include count facet if available is requested
411
+ if (criteria.options.available !== false) {
412
+ facet_stage.$facet.count = [{$count: 'total'}];
413
+ }
414
+
415
+ compiled.pipeline.push(facet_stage);
416
+
417
+ try {
418
+ let cursor = await collection.aggregate(compiled.pipeline, {});
419
+ let results = await cursor.toArray();
420
+
421
+ let facet_result = results[0] || {};
422
+ let rows = facet_result.data || [];
423
+ let available = null;
424
+
425
+ if (criteria.options.available !== false) {
426
+ available = facet_result.count?.[0]?.total || 0;
434
427
  }
435
-
436
- data.rows = that.organizeResultItems(model, data.rows);
437
-
438
- pledge.resolve(data);
439
- });
440
-
428
+
429
+ rows = that.organizeResultItems(model, rows);
430
+
431
+ pledge.resolve({rows, available});
432
+ } catch (err) {
433
+ pledge.reject(err);
434
+ }
435
+
441
436
  return pledge;
442
437
  }
443
438
 
@@ -457,13 +452,13 @@ Mongo.setMethod(function _read(context) {
457
452
  Pledge.done(cursor.toArray(), next);
458
453
  }
459
454
  }, function done(err, data) {
460
-
455
+
461
456
  if (err) {
462
- return callback(err);
457
+ return pledge.reject(err);
463
458
  }
464
-
459
+
465
460
  data.rows = that.organizeResultItems(model, data.rows);
466
-
461
+
467
462
  pledge.resolve(data);
468
463
  });
469
464
 
@@ -506,38 +501,39 @@ Mongo.setMethod(function _create(context) {
506
501
  data[key] = val;
507
502
  }
508
503
 
509
- Pledge.done(collection.insertOne(data, {w: 1, fullResult: true}), function afterInsert(err, result) {
504
+ Pledge.done(collection.insertOne(data, {w: 1}), function afterInsert(err, result) {
510
505
 
511
- // Clear the cache
512
- model.nukeCache();
506
+ // Clear the cache
507
+ model.nukeCache();
513
508
 
514
- if (err != null) {
515
- return pledge.reject(err);
516
- }
517
-
518
- // @TODO: fix because of mongodb 6
519
- let write_errors = result.message?.documents?.[0]?.writeErrors;
520
-
521
- if (write_errors) {
509
+ if (err != null) {
510
+ // In MongoDB driver 6.x, write errors are thrown as MongoServerError
511
+ // Check if this is a write error that should be converted to validation violations
512
+ if (err.code || err.writeErrors) {
522
513
  let violations = new Classes.Alchemy.Error.Validation.Violations();
523
514
 
524
- if (write_errors.length) {
525
- let entry;
526
-
527
- for (entry of write_errors) {
515
+ // Handle bulk write errors (array of errors)
516
+ if (err.writeErrors && err.writeErrors.length) {
517
+ for (let entry of err.writeErrors) {
528
518
  let violation = new Classes.Alchemy.Error.Validation.Violation();
529
- violation.message = entry.errmsg || entry.message || entry.code;
519
+ violation.message = entry.errmsg || entry.message || String(entry.code);
530
520
  violations.add(violation);
531
521
  }
532
522
  } else {
533
- violations.add(new Error('Unknown database error'));
523
+ // Single error (e.g., duplicate key)
524
+ let violation = new Classes.Alchemy.Error.Validation.Violation();
525
+ violation.message = err.errmsg || err.message || String(err.code);
526
+ violations.add(violation);
534
527
  }
535
528
 
536
529
  return pledge.reject(violations);
537
530
  }
538
531
 
539
- pledge.resolve(Object.assign({}, data));
540
- });
532
+ return pledge.reject(err);
533
+ }
534
+
535
+ pledge.resolve(Object.assign({}, data));
536
+ });
541
537
 
542
538
  return pledge;
543
539
  }
@@ -645,7 +641,7 @@ const performUpdate = (collection, model, context) => {
645
641
  }
646
642
 
647
643
  if (options.debug) {
648
- console.log('Updating with obj', id, update_object);
644
+ log.debug('Updating with obj', id, update_object);
649
645
  }
650
646
 
651
647
  let promise;