alchemymvc 1.4.0 → 1.4.2

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 (44) hide show
  1. package/lib/app/behaviour/revision_behaviour.js +1 -1
  2. package/lib/app/behaviour/sluggable_behaviour.js +2 -2
  3. package/lib/app/datasource/mongo_datasource.js +19 -3
  4. package/lib/app/helper/cron.js +2 -2
  5. package/lib/app/helper_datasource/00-nosql_datasource.js +9 -3
  6. package/lib/app/helper_datasource/05-fallback_datasource.js +10 -13
  7. package/lib/app/helper_datasource/idb_datasource.js +7 -5
  8. package/lib/app/helper_datasource/remote_datasource.js +1 -1
  9. package/lib/app/helper_field/password_field.js +4 -2
  10. package/lib/app/helper_field/schema_field.js +3 -2
  11. package/lib/app/helper_field/time_field.js +1 -1
  12. package/lib/app/helper_model/00-base_criteria.js +14 -0
  13. package/lib/app/helper_model/05-criteria_expressions.js +30 -7
  14. package/lib/app/helper_model/10-model_criteria.js +47 -8
  15. package/lib/app/helper_model/document.js +11 -2
  16. package/lib/app/helper_model/model.js +6 -3
  17. package/lib/app/model/system_task_history_model.js +134 -0
  18. package/lib/class/conduit.js +5 -2
  19. package/lib/class/controller.js +1 -0
  20. package/lib/class/datasource.js +14 -2
  21. package/lib/class/document.js +40 -12
  22. package/lib/class/import_stream_parser.js +299 -0
  23. package/lib/class/inode_file.js +2 -0
  24. package/lib/class/migration.js +5 -2
  25. package/lib/class/model.js +12 -142
  26. package/lib/class/plugin.js +32 -3
  27. package/lib/class/postponement.js +1 -1
  28. package/lib/class/router.js +26 -28
  29. package/lib/class/schema_client.js +39 -8
  30. package/lib/class/sitemap.js +2 -2
  31. package/lib/class/task.js +42 -24
  32. package/lib/core/alchemy.js +110 -162
  33. package/lib/core/alchemy_load_functions.js +64 -5
  34. package/lib/core/base.js +2 -2
  35. package/lib/core/middleware.js +31 -5
  36. package/lib/core/prefix.js +1 -1
  37. package/lib/core/setting.js +12 -9
  38. package/lib/scripts/create_constants.js +5 -1
  39. package/lib/stages/00-load_core.js +8 -2
  40. package/lib/testing/browser.js +1164 -0
  41. package/lib/testing/harness.js +922 -0
  42. package/package.json +13 -6
  43. package/testing/browser.js +27 -0
  44. 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
  }
@@ -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
 
@@ -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 {
@@ -284,7 +284,7 @@ function splitAndCleanup(input, separator) {
284
284
  * @author Santhosh Kumar <brsanthu@gmail.com>
285
285
  * @author Jelle De Loecker <jelle@elevenways.be>
286
286
  * @since 1.3.17
287
- * @version 1.3.17
287
+ * @version 1.4.1
288
288
  */
289
289
  CronExpression.setMethod(function parse() {
290
290
 
@@ -296,7 +296,7 @@ CronExpression.setMethod(function parse() {
296
296
  let has_seconds = this.has_seconds;
297
297
 
298
298
  if (Cron.PREDEFINED_EXPRESSIONS[internal_expression]) {
299
- internal_expression = Cron.PREDEFINED_EXPRESSIONS[expression];
299
+ internal_expression = Cron.PREDEFINED_EXPRESSIONS[internal_expression];
300
300
  has_seconds = false;
301
301
  }
302
302
 
@@ -706,7 +706,7 @@ function addLookupForAssociation(aggregate, assoc, assoc_model, options = {}) {
706
706
  *
707
707
  * @author Jelle De Loecker <jelle@elevenways.be>
708
708
  * @since 1.1.0
709
- * @version 1.4.0
709
+ * @version 1.4.1
710
710
  *
711
711
  * @param {Criteria} criteria The criteria to convert
712
712
  * @param {Group} group The current group
@@ -1210,6 +1210,14 @@ function convertCriteriaGroupToConditions(criteria, group, config, context) {
1210
1210
  }
1211
1211
  }
1212
1212
 
1213
+ // Skip $lookup when there are nested associations (e.g., 'Parent.Parent')
1214
+ // because $lookup only handles one level - let the N+1 code path handle nested
1215
+ let assoc_select = associations_to_select[alias];
1216
+ if (assoc_select?.associations && Object.keys(assoc_select.associations).length > 0) {
1217
+ // This association has nested associations, skip $lookup
1218
+ continue;
1219
+ }
1220
+
1213
1221
  // Only create aggregate when we actually need it
1214
1222
  getAggregate();
1215
1223
 
@@ -1228,8 +1236,6 @@ function convertCriteriaGroupToConditions(criteria, group, config, context) {
1228
1236
  };
1229
1237
 
1230
1238
  // Add projection if specific fields are selected (not just the whole association)
1231
- let assoc_select = associations_to_select[alias];
1232
-
1233
1239
  if (assoc_select && assoc_select.fields && assoc_select.fields.length > 0) {
1234
1240
  let projection = { _id: 1 }; // Always include _id
1235
1241
  for (let field of assoc_select.fields) {
@@ -94,6 +94,8 @@ Fallback.setMethod(function storeInUpperDatasource(model, data, options) {
94
94
  data._$cache_time = Date.now();
95
95
 
96
96
  // Also mark it as being a local save if needed
97
+ let time;
98
+
97
99
  if (options.local_save) {
98
100
  if (options.local_save === true) {
99
101
  time = Date.now();
@@ -156,7 +158,7 @@ Fallback.setMethod(function storeInUpperDatasource(model, data, options) {
156
158
  *
157
159
  * @author Jelle De Loecker <jelle@elevenways.be>
158
160
  * @since 1.1.0
159
- * @version 1.4.0
161
+ * @version 1.4.2
160
162
  *
161
163
  * @param {Alchemy.OperationalContext.ReadDocumentFromDatasource} context
162
164
  *
@@ -174,7 +176,7 @@ Fallback.setMethod(function read(context) {
174
176
  let lower_context = context.createChild();
175
177
  lower_context.setCriteria(lower_criteria);
176
178
 
177
- tasks.push(() => this.lower.read(context));
179
+ tasks.push(() => this.lower.read(lower_context));
178
180
  }
179
181
 
180
182
  let upper_criteria = criteria.clone();
@@ -193,7 +195,7 @@ Fallback.setMethod(function read(context) {
193
195
  *
194
196
  * @author Jelle De Loecker <jelle@elevenways.be>
195
197
  * @since 1.1.0
196
- * @version 1.1.0
198
+ * @version 1.4.2
197
199
  *
198
200
  * @param {Model} model
199
201
  *
@@ -202,21 +204,16 @@ Fallback.setMethod(function read(context) {
202
204
  Fallback.setMethod(function getRecordsToSync(model) {
203
205
 
204
206
  var that = this,
205
- pledge = new Pledge,
206
207
  criteria = model.find();
207
208
 
208
209
  criteria.where('_$needs_remote_save').equals(1);
209
210
 
210
- this.upper.read(model, criteria, function gotRecords(err, records) {
211
+ let context = new Classes.Alchemy.OperationalContext.ReadDocumentFromDatasource();
212
+ context.setDatasource(this.upper);
213
+ context.setModel(model);
214
+ context.setCriteria(criteria);
211
215
 
212
- if (err) {
213
- return pledge.reject(err);
214
- }
215
-
216
- pledge.resolve(records);
217
- });
218
-
219
- return pledge;
216
+ return this.upper.read(context);
220
217
  });
221
218
 
222
219
  /**
@@ -156,15 +156,16 @@ Idb.setMethod(function _read(context) {
156
156
  *
157
157
  * @author Jelle De Loecker <jelle@elevenways.be>
158
158
  * @since 1.1.0
159
- * @version 1.1.0
159
+ * @version 1.4.0
160
160
  */
161
161
  Idb.setMethod(function _ensureIndex(model, index, callback) {
162
162
 
163
- var that = this,
164
- collection;
165
-
163
+ // Disabled for now
166
164
  return callback();
167
165
 
166
+ let that = this,
167
+ collection;
168
+
168
169
  return Function.series(function getCollection(next) {
169
170
  that.collection(model.table).done(next);
170
171
  }, function checkIndex(next, _c) {
@@ -181,9 +182,10 @@ Idb.setMethod(function _ensureIndex(model, index, callback) {
181
182
 
182
183
  if (err) {
183
184
  console.error('Failed to create IDB index:', err);
184
- return;
185
+ return callback(err);
185
186
  }
186
187
 
188
+ callback();
187
189
  });
188
190
 
189
191
  });
@@ -81,7 +81,7 @@ Remote.setMethod(async function doServerCommand(action, model, data, callback) {
81
81
  let fetch_options = {
82
82
  post : data,
83
83
  headers : {'content-type': 'application/json-dry'},
84
- max_timeout : 3500 // A timeout of max 3.5s
84
+ max_timeout : this.options.max_timeout ?? 3500 // Configurable timeout, default 3.5s
85
85
  };
86
86
 
87
87
  alchemy.fetch(route_name, fetch_options, function gotResult(err, result) {
@@ -21,7 +21,7 @@ const bcrypt = alchemy.use('bcrypt'),
21
21
  *
22
22
  * @author Jelle De Loecker <jelle@elevenways.be>
23
23
  * @since 0.4.0
24
- * @version 1.4.0
24
+ * @version 1.4.1
25
25
  *
26
26
  * @param {Alchemy.OperationalContext.SaveFieldToDatasource} context
27
27
  * @param {*} value
@@ -36,7 +36,9 @@ Password.setMethod(function _toDatasource(context, value) {
36
36
 
37
37
  let pledge = new Pledge.Swift();
38
38
 
39
- bcrypt.hash(value, 10, pledge.getResolverFunction());
39
+ let salt_rounds = this.options.salt_rounds || 10;
40
+
41
+ bcrypt.hash(value, salt_rounds, pledge.getResolverFunction());
40
42
 
41
43
  return pledge;
42
44
  });
@@ -439,6 +439,7 @@ SchemaField.setMethod(function _toDatasourceFromValueWithSubSchema(context, valu
439
439
  model = Model.get(model);
440
440
 
441
441
  let pledge = new Pledge.Swift();
442
+ let that = this;
442
443
 
443
444
  model.find('first', {document: false, fields: [this.options.schema]}, function gotRecord(err, result) {
444
445
 
@@ -482,8 +483,8 @@ SchemaField.setMethod(function _toDatasourceFromValueWithSubSchema(context, valu
482
483
  }
483
484
  }
484
485
 
485
- log.warning('Model and subschema were not found for', that.path);
486
- return Pledge.Swift.done(datasource.toDatasource(context), callback);
486
+ log.warning('Model and subschema were not found for', this.path);
487
+ return datasource.toDatasource(context);
487
488
  });
488
489
 
489
490
  /**
@@ -8,7 +8,7 @@
8
8
  * @version 1.1.0
9
9
  */
10
10
  var Time = Function.inherits('Alchemy.Field', function Time(schema, name, options) {
11
- Time.call(this, schema, name, options);
11
+ Time.super.call(this, schema, name, options);
12
12
  });
13
13
 
14
14
  /**
@@ -359,6 +359,20 @@ Criteria.setMethod(function compileToConditions(context) {
359
359
  return Classes.Alchemy.Datasource.Nosql.convertCriteriaToConditions(this, context);
360
360
  });
361
361
 
362
+ /**
363
+ * Get the associations to select (stub for base Criteria)
364
+ *
365
+ * @author Jelle De Loecker <jelle@elevenways.be>
366
+ * @since 1.4.1
367
+ * @version 1.4.1
368
+ *
369
+ * @return {Object|undefined}
370
+ */
371
+ Criteria.setMethod(function getAssociationsToSelect() {
372
+ // Base criteria doesn't have associations
373
+ return;
374
+ });
375
+
362
376
  /**
363
377
  * Apply old-style mongodb conditions
364
378
  *
@@ -366,7 +366,7 @@ FieldExpression.setMethod(Blast.checksumSymbol, function toChecksum() {
366
366
  *
367
367
  * @author Jelle De Loecker <jelle@elevenways.be>
368
368
  * @since 1.1.0
369
- * @version 1.1.0
369
+ * @version 1.4.1
370
370
  *
371
371
  * @param {string} path
372
372
  */
@@ -389,10 +389,28 @@ FieldExpression.setMethod(function setTargetPath(path) {
389
389
  } else {
390
390
  let first = pieces[0];
391
391
 
392
- // @TODO: better check if the first part is an association
392
+ // Check if the first part is an association
393
+ // First use the uppercase heuristic, then validate against the model if available
393
394
  if (first[0].isUpperCase()) {
394
- this.association = first;
395
- pieces.shift();
395
+ let is_association = true;
396
+
397
+ // If the model is available, verify this is actually an association
398
+ let model = this.criteria?.model;
399
+
400
+ if (model) {
401
+ try {
402
+ let assoc = model.getAssociation(first);
403
+ is_association = !!assoc;
404
+ } catch (err) {
405
+ // Association doesn't exist - treat as a field name
406
+ is_association = false;
407
+ }
408
+ }
409
+
410
+ if (is_association) {
411
+ this.association = first;
412
+ pieces.shift();
413
+ }
396
414
  }
397
415
 
398
416
  this.target_path = pieces.join('.');
@@ -487,7 +505,7 @@ FieldExpression.setMethod(function normalize() {
487
505
  *
488
506
  * @author Jelle De Loecker <jelle@elevenways.be>
489
507
  * @since 1.1.0
490
- * @version 1.4.0
508
+ * @version 1.4.1
491
509
  *
492
510
  * @return {Pledge}
493
511
  */
@@ -498,8 +516,13 @@ FieldExpression.setMethod(function normalizeAssociationValues() {
498
516
  }
499
517
 
500
518
  let that = this,
501
- association = this.model.getAssociation(this.association),
502
- assoc_model = this.model.getModel(association.modelName),
519
+ association = this.model.getAssociation(this.association);
520
+
521
+ if (!association) {
522
+ return Pledge.reject(new Error('Association "' + this.association + '" not found on model "' + this.model.name + '"'));
523
+ }
524
+
525
+ let assoc_model = this.model.getModel(association.modelName),
503
526
  assoc_crit = new Classes.Alchemy.Criteria.Model(),
504
527
  pledge = new Pledge(),
505
528
  clone = this.getCleanClone(),
@@ -398,7 +398,7 @@ Criteria.setMethod(function getAssociationConfiguration(alias) {
398
398
  *
399
399
  * @author Jelle De Loecker <jelle@elevenways.be>
400
400
  * @since 1.1.0
401
- * @version 1.4.0
401
+ * @version 1.4.1
402
402
  *
403
403
  * @param {string} name
404
404
  * @param {Object} item
@@ -426,14 +426,20 @@ Criteria.setMethod(function getCriteriaForAssociation(name, item) {
426
426
  options = this.options;
427
427
 
428
428
  // For self-referencing associations (e.g., Person.Parent → Person),
429
- // only allow if we have recursive depth remaining.
429
+ // only allow if we have recursive depth remaining OR explicit nested associations.
430
430
  // This prevents infinite loops while still supporting hierarchical data.
431
431
  if (assoc_model.name == options.init_model) {
432
432
  // Check if we have recursive depth available
433
433
  let recursive = options.recursive;
434
434
 
435
- if (!Number.isSafeInteger(recursive) || recursive <= 0) {
436
- // No recursive depth - block to prevent potential infinite loops
435
+ // Also check for explicit nested associations via dot notation (e.g., 'Parent.Parent')
436
+ // If there's an explicit Select for this association with nested associations,
437
+ // we should allow it even without recursive depth
438
+ let has_explicit_nested = options.select?.associations?.[name]?.associations;
439
+ let has_nested_associations = has_explicit_nested && Object.keys(has_explicit_nested).length > 0;
440
+
441
+ if (!has_nested_associations && (!Number.isSafeInteger(recursive) || recursive <= 0)) {
442
+ // No recursive depth and no explicit nested associations - block to prevent infinite loops
437
443
  return;
438
444
  }
439
445
  }
@@ -985,12 +991,34 @@ Select.setMethod(Blast.checksumSymbol, function toChecksum() {
985
991
  return result;
986
992
  });
987
993
 
994
+ /**
995
+ * Get the model to use for association lookups.
996
+ * For nested Selects (e.g., when populating 'Project.Client'),
997
+ * this returns the associated model, not the criteria's root model.
998
+ *
999
+ * @author Jelle De Loecker <jelle@elevenways.be>
1000
+ * @since 1.4.1
1001
+ * @version 1.4.1
1002
+ *
1003
+ * @return {Model}
1004
+ */
1005
+ Select.setMethod(function getModelForAssociations() {
1006
+
1007
+ // If this Select has an associated model set (for nested associations),
1008
+ // use that instead of the criteria's model
1009
+ if (this.associated_model) {
1010
+ return this.associated_model;
1011
+ }
1012
+
1013
+ return this.criteria?.model;
1014
+ });
1015
+
988
1016
  /**
989
1017
  * Add an association
990
1018
  *
991
1019
  * @author Jelle De Loecker <jelle@elevenways.be>
992
1020
  * @since 1.1.0
993
- * @version 1.3.4
1021
+ * @version 1.4.1
994
1022
  *
995
1023
  * @param {string} name
996
1024
  *
@@ -998,7 +1026,9 @@ Select.setMethod(Blast.checksumSymbol, function toChecksum() {
998
1026
  */
999
1027
  Select.setMethod(function addAssociation(name) {
1000
1028
 
1001
- if (!this.criteria?.model) {
1029
+ let model = this.getModelForAssociations();
1030
+
1031
+ if (!model) {
1002
1032
  throw new Error('Unable to select an association: this Criteria has no model info');
1003
1033
  }
1004
1034
 
@@ -1032,14 +1062,23 @@ Select.setMethod(function addAssociation(name) {
1032
1062
 
1033
1063
  // Get the association data
1034
1064
  try {
1035
- let info = this.criteria.model.getAssociation(name);
1065
+ let info = model.getAssociation(name);
1036
1066
 
1037
1067
  if (info) {
1038
1068
  // Make sure the localkey is added to the resultset
1039
1069
  this.requireFieldForQuery(info.options.localKey);
1070
+
1071
+ // Store the associated model on the nested Select so it can
1072
+ // resolve its own nested associations correctly
1073
+ // (e.g., for 'Project.Client', the Project Select needs to know
1074
+ // to look up 'Client' on the Project model, not the root model)
1075
+ let associated_model = this.getModel(info.modelName);
1076
+ if (associated_model) {
1077
+ this.associations[name].associated_model = associated_model;
1078
+ }
1040
1079
  }
1041
1080
  } catch (err) {
1042
- console.warn('Failed to find "' + name + '" association for ' + this.criteria.model.model_name);
1081
+ console.warn('Failed to find "' + name + '" association for ' + model.model_name);
1043
1082
  }
1044
1083
 
1045
1084
  return this.associations[name];
@@ -1282,7 +1282,7 @@ Document.setMethod(function checkAndInformDatasource(options, callback) {
1282
1282
  *
1283
1283
  * @author Jelle De Loecker <jelle@elevenways.be>
1284
1284
  * @since 1.0.0
1285
- * @version 1.1.0
1285
+ * @version 1.4.1
1286
1286
  *
1287
1287
  * @param {Object} options
1288
1288
  * @param {Function} callback
@@ -1319,7 +1319,7 @@ Document.setMethod(function informDatasource(options, callback) {
1319
1319
 
1320
1320
  cache_stores_in_progress.set(this._id, true);
1321
1321
 
1322
- return Function.series(async function getLocalVersion(next) {
1322
+ let series_result = Function.series(async function getLocalVersion(next) {
1323
1323
 
1324
1324
  if (is_save) {
1325
1325
  return next();
@@ -1367,6 +1367,15 @@ Document.setMethod(function informDatasource(options, callback) {
1367
1367
 
1368
1368
  callback.apply(null, results.last());
1369
1369
  });
1370
+
1371
+ // Ensure cleanup happens even if the series fails unexpectedly
1372
+ if (series_result && typeof series_result.catch === 'function') {
1373
+ series_result.catch(() => {
1374
+ cache_stores_in_progress.delete(that._id);
1375
+ });
1376
+ }
1377
+
1378
+ return series_result;
1370
1379
  });
1371
1380
 
1372
1381
  /**
@@ -1362,7 +1362,7 @@ Model.setMethod(function afterFindCount(options, records, callback) {
1362
1362
  *
1363
1363
  * @author Jelle De Loecker <jelle@elevenways.be>
1364
1364
  * @since 0.1.0
1365
- * @version 1.1.5
1365
+ * @version 1.4.1
1366
1366
  *
1367
1367
  * @param {Criteria} criteria
1368
1368
  * @param {Object} item
@@ -1404,13 +1404,16 @@ Model.setMethod(function addAssociatedDataToRecord(criteria, item, callback) {
1404
1404
  if (Object.isPlainObject(item[alias])) {
1405
1405
  let assoc_crit = criteria.getCriteriaForAssociation(alias, item);
1406
1406
 
1407
- if (!(item[alias] instanceof assoc_crit.model.constructor.Document)) {
1407
+ // assoc_crit can be undefined for self-referencing without recursive depth
1408
+ if (assoc_crit && !(item[alias] instanceof assoc_crit.model.constructor.Document)) {
1408
1409
  item[alias] = assoc_crit.model.createDocument(item[alias]);
1409
1410
  }
1410
1411
  }
1411
1412
  } else if (Array.isArray(item[alias])) {
1412
1413
  let assoc_crit = criteria.getCriteriaForAssociation(alias, item);
1413
- item[alias] = assoc_crit.model.createDocumentList(item[alias]);
1414
+ if (assoc_crit) {
1415
+ item[alias] = assoc_crit.model.createDocumentList(item[alias]);
1416
+ }
1414
1417
  }
1415
1418
  }
1416
1419