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.
- package/lib/app/behaviour/revision_behaviour.js +1 -1
- package/lib/app/behaviour/sluggable_behaviour.js +2 -2
- package/lib/app/datasource/mongo_datasource.js +19 -3
- package/lib/app/helper/cron.js +2 -2
- package/lib/app/helper_datasource/00-nosql_datasource.js +9 -3
- package/lib/app/helper_datasource/05-fallback_datasource.js +10 -13
- package/lib/app/helper_datasource/idb_datasource.js +7 -5
- package/lib/app/helper_datasource/remote_datasource.js +1 -1
- package/lib/app/helper_field/password_field.js +4 -2
- package/lib/app/helper_field/schema_field.js +3 -2
- package/lib/app/helper_field/time_field.js +1 -1
- package/lib/app/helper_model/00-base_criteria.js +14 -0
- package/lib/app/helper_model/05-criteria_expressions.js +30 -7
- package/lib/app/helper_model/10-model_criteria.js +47 -8
- package/lib/app/helper_model/document.js +11 -2
- package/lib/app/helper_model/model.js +6 -3
- package/lib/app/model/system_task_history_model.js +134 -0
- package/lib/class/conduit.js +5 -2
- package/lib/class/controller.js +1 -0
- package/lib/class/datasource.js +14 -2
- package/lib/class/document.js +40 -12
- package/lib/class/import_stream_parser.js +299 -0
- package/lib/class/inode_file.js +2 -0
- package/lib/class/migration.js +5 -2
- package/lib/class/model.js +12 -142
- package/lib/class/plugin.js +32 -3
- package/lib/class/postponement.js +1 -1
- package/lib/class/router.js +26 -28
- package/lib/class/schema_client.js +39 -8
- package/lib/class/sitemap.js +2 -2
- package/lib/class/task.js +42 -24
- package/lib/core/alchemy.js +110 -162
- package/lib/core/alchemy_load_functions.js +64 -5
- package/lib/core/base.js +2 -2
- package/lib/core/middleware.js +31 -5
- package/lib/core/prefix.js +1 -1
- package/lib/core/setting.js +12 -9
- package/lib/scripts/create_constants.js +5 -1
- package/lib/stages/00-load_core.js +8 -2
- package/lib/testing/browser.js +1164 -0
- package/lib/testing/harness.js +922 -0
- package/package.json +13 -6
- package/testing/browser.js +27 -0
- 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.
|
|
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 (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 {
|
package/lib/app/helper/cron.js
CHANGED
|
@@ -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.
|
|
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[
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
-
|
|
211
|
+
let context = new Classes.Alchemy.OperationalContext.ReadDocumentFromDatasource();
|
|
212
|
+
context.setDatasource(this.upper);
|
|
213
|
+
context.setModel(model);
|
|
214
|
+
context.setCriteria(criteria);
|
|
211
215
|
|
|
212
|
-
|
|
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.
|
|
159
|
+
* @version 1.4.0
|
|
160
160
|
*/
|
|
161
161
|
Idb.setMethod(function _ensureIndex(model, index, callback) {
|
|
162
162
|
|
|
163
|
-
|
|
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 //
|
|
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.
|
|
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
|
-
|
|
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',
|
|
486
|
-
return
|
|
486
|
+
log.warning('Model and subschema were not found for', this.path);
|
|
487
|
+
return datasource.toDatasource(context);
|
|
487
488
|
});
|
|
488
489
|
|
|
489
490
|
/**
|
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
|
|
395
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
436
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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 ' +
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1414
|
+
if (assoc_crit) {
|
|
1415
|
+
item[alias] = assoc_crit.model.createDocumentList(item[alias]);
|
|
1416
|
+
}
|
|
1414
1417
|
}
|
|
1415
1418
|
}
|
|
1416
1419
|
|