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
@@ -390,7 +390,7 @@ Model.setStatic(async function checkPathValue(value, name, field_name, conduit)
390
390
  if (result) {
391
391
  let found_value = result[field_name];
392
392
 
393
- if (found_value != value && !Object.alike(value, found_value)) {
393
+ if (conduit && found_value != value && !Object.alike(value, found_value)) {
394
394
  conduit.rewriteRequestRouteParam(name, found_value);
395
395
  }
396
396
  }
@@ -534,7 +534,7 @@ Model.setStatic(function getField(name) {
534
534
  if (name.indexOf('.') > -1) {
535
535
  split = name.split('.');
536
536
 
537
- alias = name[0];
537
+ alias = split[0];
538
538
 
539
539
  if (this.schema.associations[alias] == null) {
540
540
  model = this;
@@ -1590,7 +1590,7 @@ Model.setMethod(function exportToStream(output, options) {
1590
1590
  *
1591
1591
  * @author Jelle De Loecker <jelle@elevenways.be>
1592
1592
  * @since 1.0.5
1593
- * @version 1.0.5
1593
+ * @version 1.4.1
1594
1594
  *
1595
1595
  * @param {Stream} input
1596
1596
  * @param {Object} options
@@ -1612,150 +1612,20 @@ Model.setMethod(function importFromStream(input, options) {
1612
1612
  return Pledge.reject(new Error('No source input stream has been given'));
1613
1613
  }
1614
1614
 
1615
- let that = this,
1616
- current_type = null,
1617
- extra_stream,
1618
- pledge = new Pledge(),
1619
- stopped,
1620
- paused,
1621
- buffer,
1622
- value,
1623
- seen = 0,
1624
- left,
1625
- size,
1626
- doc;
1627
-
1628
- input.on('data', function onData(data) {
1629
-
1630
- if (stopped) {
1631
- return;
1632
- }
1615
+ let that = this;
1616
+ let parser = new Classes.Alchemy.ImportStreamParser(input, options);
1633
1617
 
1634
- if (buffer) {
1635
- buffer = Buffer.concat([buffer, data]);
1636
- } else {
1637
- buffer = data;
1618
+ // Model resolver for single-model import:
1619
+ // Validates that the model name matches, returns this model instance
1620
+ parser.setModelResolver(function resolveModel(model_name, current_model) {
1621
+ if (model_name == that.model_name) {
1622
+ return that;
1638
1623
  }
1639
1624
 
1640
- handleBuffer();
1625
+ throw new Error('Model names do not match: expected "' + that.model_name + '", got "' + model_name + '"');
1641
1626
  });
1642
1627
 
1643
- function handleBuffer() {
1644
-
1645
- if (paused) {
1646
- return;
1647
- }
1648
-
1649
- if (!current_type && buffer.length < 2) {
1650
- return;
1651
- }
1652
-
1653
- if (!current_type) {
1654
- current_type = buffer.readUInt8(0);
1655
-
1656
- if (current_type == 0x01) {
1657
- size = buffer.readUInt8(1);
1658
- buffer = buffer.slice(2);
1659
- } else if (current_type == 0x02 && buffer.length >= 5) {
1660
- size = buffer.readUInt32BE(1);
1661
- buffer = buffer.slice(5);
1662
- } else if (current_type == 0xFF) {
1663
- size = buffer.readUInt32BE(1);
1664
- buffer = buffer.slice(5);
1665
- seen = 0;
1666
-
1667
- if (!doc) {
1668
- stopped = true;
1669
- pledge.reject(new Error('Found extra import data, but no active document'));
1670
- } else {
1671
- extra_stream = new require('stream').PassThrough();
1672
- doc.extraImportFromStream(extra_stream);
1673
- }
1674
- } else {
1675
- // Not enough data? Wait
1676
- current_type = null;
1677
- return;
1678
- }
1679
- }
1680
-
1681
- handleRest();
1682
- }
1683
-
1684
- function handleRest() {
1685
-
1686
- if (current_type == 0xFF) {
1687
- left = size - seen;
1688
- value = buffer.slice(0, left);
1689
-
1690
- seen += value.length;
1691
-
1692
- if (value.length == buffer.length) {
1693
- buffer = null;
1694
- } else if (value.length < buffer.length) {
1695
- buffer = buffer.slice(left);
1696
- }
1697
-
1698
- extra_stream.write(value);
1699
-
1700
- if (value.length == left) {
1701
- extra_stream.end();
1702
- current_type = null;
1703
-
1704
- if (buffer) {
1705
- handleBuffer();
1706
- }
1707
- }
1708
-
1709
- return;
1710
- }
1711
-
1712
- if (buffer.length >= size) {
1713
- value = buffer.slice(0, size);
1714
- buffer = buffer.slice(size);
1715
- } else {
1716
- // Wait for next call
1717
- return;
1718
- }
1719
-
1720
- if (current_type == 0x01) {
1721
- value = value.toString();
1722
-
1723
- if (value == that.model_name) {
1724
- // Found name!
1725
- current_type = null;
1726
- size = 0;
1727
- } else {
1728
- stopped = true;
1729
- return pledge.reject(new Error('Model names do not match'));
1730
- }
1731
- } else if (current_type == 0x02) {
1732
- doc = that.createDocument();
1733
- input.pause();
1734
- paused = true;
1735
-
1736
- doc.importFromBuffer(value).done(function done(err, result) {
1737
-
1738
- if (err) {
1739
- stopped = true;
1740
- return pledge.reject(err);
1741
- }
1742
-
1743
- current_type = null;
1744
- paused = false;
1745
- input.resume();
1746
-
1747
- handleBuffer();
1748
- });
1749
-
1750
- return;
1751
- }
1752
-
1753
- if (buffer && buffer.length) {
1754
- handleBuffer();
1755
- }
1756
- }
1757
-
1758
- return pledge;
1628
+ return parser.parse();
1759
1629
  });
1760
1630
 
1761
1631
  /**
@@ -41,7 +41,7 @@ const Plugin = Function.inherits('Alchemy.Base', function Plugin(name, path, def
41
41
  *
42
42
  * @author Jelle De Loecker <jelle@elevenways.be>
43
43
  * @since 1.4.0
44
- * @version 1.4.0
44
+ * @version 1.4.1
45
45
  *
46
46
  * @return {boolean}
47
47
  */
@@ -52,16 +52,45 @@ Plugin.setMethod(function doPreload() {
52
52
  }
53
53
 
54
54
  // Create settings from the `config/settings.js` file
55
- this.loadSettingDefinitions();
55
+ // This can fail if the config is invalid
56
+ try {
57
+ this.loadSettingDefinitions();
58
+ } catch (err) {
59
+ this._handleLoadError('settings', err);
60
+ return false;
61
+ }
56
62
 
57
63
  // Load the bootstrap file
58
- this.loadBootstrap();
64
+ // This can fail due to syntax errors or runtime errors
65
+ try {
66
+ this.loadBootstrap();
67
+ } catch (err) {
68
+ this._handleLoadError('bootstrap', err);
69
+ return false;
70
+ }
71
+
72
+ this[FLAGS].preloaded = true;
59
73
 
60
74
  PLUGINS_STAGE.addPostTask(() => {
61
75
  return this.startPlugin();
62
76
  });
63
77
  });
64
78
 
79
+ /**
80
+ * Handle a plugin loading error
81
+ * Plugin errors are fatal - the server cannot start with a broken plugin
82
+ *
83
+ * @author Jelle De Loecker <jelle@elevenways.be>
84
+ * @since 1.4.1
85
+ * @version 1.4.1
86
+ *
87
+ * @param {string} phase Which phase failed (settings, bootstrap, start)
88
+ * @param {Error} err The error that occurred
89
+ */
90
+ Plugin.setMethod(function _handleLoadError(phase, err) {
91
+ alchemy.handlePluginError(this.name, phase, err);
92
+ });
93
+
65
94
  /**
66
95
  * Do the rest of the plugin loading
67
96
  *
@@ -466,7 +466,7 @@ Postponement.setMethod(function putInQueue() {
466
466
  return;
467
467
  }
468
468
 
469
- let new_length = QUEUE.push(this) - 1;
469
+ let new_length = QUEUE.push(this);
470
470
 
471
471
  this.last_queue_position = new_length - 1;
472
472
 
@@ -200,7 +200,7 @@ RouterClass.setMethod(function headerBypass(prefix) {
200
200
  *
201
201
  * @author Jelle De Loecker <jelle@elevenways.be>
202
202
  * @since 0.3.0
203
- * @version 0.3.0
203
+ * @version 1.4.1
204
204
  */
205
205
  RouterClass.setMethod(function getFullMount() {
206
206
 
@@ -215,6 +215,11 @@ RouterClass.setMethod(function getFullMount() {
215
215
  }
216
216
  }
217
217
 
218
+ // Remove trailing slash (but keep single "/" for root)
219
+ if (result.length > 1 && result[result.length - 1] == '/') {
220
+ result = result.slice(0, -1);
221
+ }
222
+
218
223
  return result;
219
224
  });
220
225
 
@@ -228,7 +233,8 @@ RouterClass.setMethod(function getFullMount() {
228
233
  RouterClass.setMethod(function getPrefix(path) {
229
234
 
230
235
  var prefix,
231
- begin;
236
+ begin,
237
+ key;
232
238
 
233
239
  // See if the path starts with any set prefix
234
240
  for (key in prefixes) {
@@ -1053,34 +1059,12 @@ RouterClass.setMethod('delete', function _delete(name, paths, fnc, options) {
1053
1059
  return this.add(['delete'], name, paths, fnc, options);
1054
1060
  });
1055
1061
 
1056
- /**
1057
- * Get an object of all the routes in this router and its children
1058
- *
1059
- * @author Jelle De Loecker <jelle@elevenways.be>
1060
- * @since 0.2.0
1061
- * @version 0.2.0
1062
- */
1063
- RouterClass.setMethod(function getFullMount() {
1064
-
1065
- var result = this.mount;
1066
-
1067
- if (this.parent != null && this.parent.mount != '/') {
1068
- result = this.parent.mount + result;
1069
- }
1070
-
1071
- if (result[result.length-1] == '/') {
1072
- result = result.slice(0, -1);
1073
- }
1074
-
1075
- return result;
1076
- });
1077
-
1078
1062
  /**
1079
1063
  * Get the full route object, for internal use
1080
1064
  *
1081
1065
  * @author Jelle De Loecker <jelle@elevenways.be>
1082
1066
  * @since 0.5.0
1083
- * @version 0.5.0
1067
+ * @version 1.4.1
1084
1068
  *
1085
1069
  * @param {Object} result Optional object to store sectioned results in
1086
1070
  *
@@ -1111,7 +1095,14 @@ RouterClass.setMethod(function getFullRoutes(result) {
1111
1095
  temp = {};
1112
1096
 
1113
1097
  for (prefix in route.paths) {
1114
- temp[prefix] = mount + route.paths[prefix].source;
1098
+ let source = route.paths[prefix].source;
1099
+
1100
+ // Avoid double slashes when mount is '/' and path starts with '/'
1101
+ if (mount == '/' && source[0] == '/') {
1102
+ temp[prefix] = source;
1103
+ } else {
1104
+ temp[prefix] = mount + source;
1105
+ }
1115
1106
  }
1116
1107
 
1117
1108
  section[route.name] = route;
@@ -1175,7 +1166,7 @@ RouterClass.setMethod(function getOptions(result) {
1175
1166
  *
1176
1167
  * @author Jelle De Loecker <jelle@elevenways.be>
1177
1168
  * @since 0.2.0
1178
- * @version 1.3.21
1169
+ * @version 1.4.1
1179
1170
  *
1180
1171
  * @param {Object} result Optional object to store sectioned results in
1181
1172
  *
@@ -1206,7 +1197,14 @@ RouterClass.setMethod(function getRoutes(result) {
1206
1197
  temp = {};
1207
1198
 
1208
1199
  for (prefix in route.paths) {
1209
- temp[prefix] = mount + route.paths[prefix].source;
1200
+ let source = route.paths[prefix].source;
1201
+
1202
+ // Avoid double slashes when mount is '/' and path starts with '/'
1203
+ if (mount == '/' && source[0] == '/') {
1204
+ temp[prefix] = source;
1205
+ } else {
1206
+ temp[prefix] = mount + source;
1207
+ }
1210
1208
  }
1211
1209
 
1212
1210
  section[route.name] = {
@@ -1079,7 +1079,7 @@ Schema.setMethod(function getFieldNames() {
1079
1079
  *
1080
1080
  * @author Jelle De Loecker <jelle@elevenways.be>
1081
1081
  * @since 0.2.0
1082
- * @version 1.4.0
1082
+ * @version 1.4.1
1083
1083
  *
1084
1084
  * @param {string|FieldType} _field_or_name Field name, or index name when using `fields` option
1085
1085
  * @param {Object} options
@@ -1111,15 +1111,46 @@ Schema.setMethod(function addIndex(_field_or_name, _options) {
1111
1111
  // When `fields` is provided, the first argument is the index name
1112
1112
  options.name = _field_or_name;
1113
1113
 
1114
+ // Create the index entry
1115
+ if (this.indexes[options.name] == null) {
1116
+ this.indexes[options.name] = {
1117
+ fields: {},
1118
+ options: options
1119
+ };
1120
+ }
1121
+
1122
+ // Add all fields to the index
1114
1123
  for (let field_name of options.fields) {
1115
- this.addIndex(field_name, {
1116
- name: options.name,
1117
- unique: options.unique,
1118
- sparse: options.sparse,
1119
- order: options.order,
1120
- });
1124
+ let field = this.getField(field_name);
1125
+
1126
+ if (!field) {
1127
+ throw new Error('Could not find field "' + field_name + '" for compound index "' + options.name + '"');
1128
+ }
1129
+
1130
+ let path = field.path;
1131
+ this.indexes[options.name].fields[path] = options.order || 1;
1132
+ this.index_fields[path] = options;
1121
1133
  }
1122
1134
 
1135
+ // Now call ensureIndex once with the complete compound index
1136
+ const that = this;
1137
+
1138
+ that.getDatasource().done(function gotDs(err, datasource) {
1139
+ if (err) {
1140
+ throw err;
1141
+ }
1142
+
1143
+ if (datasource.supports('ensure_index') === false) {
1144
+ return;
1145
+ }
1146
+
1147
+ datasource.ensureIndex(that.model_class, that.indexes[options.name], function ensuredIndex(err, result) {
1148
+ if (err) {
1149
+ alchemy.printLog('error', ['Error ensuring compound index', options.name, 'in model', that.model_name], {err: err});
1150
+ }
1151
+ });
1152
+ });
1153
+
1123
1154
  return;
1124
1155
  }
1125
1156
 
@@ -1153,7 +1184,7 @@ Schema.setMethod(function addIndex(_field_or_name, _options) {
1153
1184
  }
1154
1185
  };
1155
1186
 
1156
- if (typeof options.order == 'number') {
1187
+ if (typeof options.order == 'string') {
1157
1188
  if (options.order == 'asc') {
1158
1189
  options.order = 1;
1159
1190
  } else {
@@ -353,7 +353,7 @@ Sitemap.setMethod(function addRouteWithParameters(route, config, parameters, for
353
353
  *
354
354
  * @author Jelle De Loecker <jelle@elevenways.be>
355
355
  * @since 1.3.4
356
- * @version 1.3.6
356
+ * @version 1.4.1
357
357
  *
358
358
  * @param {string} prefix
359
359
  *
@@ -378,7 +378,7 @@ Sitemap.setMethod(function getXmlBuilder(prefix) {
378
378
  }
379
379
 
380
380
  for (let info of category.pages) {
381
- let url = urlset.ele(url);
381
+ let url = urlset.ele('url');
382
382
 
383
383
  url.ele('loc').txt(''+info.url);
384
384
 
package/lib/class/task.js CHANGED
@@ -371,33 +371,51 @@ Task.setMethod(async function start(payload) {
371
371
  let result;
372
372
 
373
373
  try {
374
- result = await this.executor();
375
- } catch (err) {
376
- if (err == 'stopped') {
377
- this.report('stopped', 'Stopped');
378
- return;
374
+ try {
375
+ result = await this.executor();
376
+ } catch (err) {
377
+ if (err == 'stopped') {
378
+ this.report('stopped', 'Stopped');
379
+ return;
380
+ }
381
+
382
+ // Set the error
383
+ this.error = err;
384
+
385
+ // Report failed
386
+ let report = this.report('failed');
387
+ report.error = err;
388
+
389
+ // Surface the failure on the history document - without
390
+ // this, the row is left with `is_running: true` and no
391
+ // error info, looking identical to a still-running task.
392
+ document.had_error = true;
393
+ document.error_message = String(err && err.message || err);
394
+ document.error_stack = err && err.stack || null;
395
+
396
+ throw err;
379
397
  }
380
398
 
381
- // Set the error
382
- this.error = err;
383
-
384
- // Report failed
385
- let report = this.report('failed');
386
- report.error = err;
387
-
388
- throw err;
389
- }
390
-
391
- // If the command hasn't been manually stopped,
392
- // report it as done
393
- if (!this.manual_stop_end) {
394
- this.report('done');
399
+ // If the command hasn't been manually stopped,
400
+ // report it as done
401
+ if (!this.manual_stop_end) {
402
+ this.report('done');
403
+ }
404
+ } finally {
405
+ // Always close out the history document so it doesn't get
406
+ // stuck as a zombie "running forever" row.
407
+ document.ended_at = new Date();
408
+ document.is_running = false;
409
+
410
+ try {
411
+ await document.save();
412
+ } catch (save_err) {
413
+ // Don't let a history-save failure swallow the original
414
+ // executor error on its way out of the catch above.
415
+ alchemy.registerError(save_err);
416
+ }
395
417
  }
396
418
 
397
- document.ended_at = new Date();
398
- document.is_running = false;
399
- await document.save();
400
-
401
419
  this[RUNNING_PLEDGE].resolve(result);
402
420
 
403
421
  return result;
@@ -567,7 +585,7 @@ async function doAsyncLoopUntilNotBusy(max_tries) {
567
585
  }
568
586
 
569
587
  do {
570
- console.log('Waiting for system to be less busy', tries)
588
+ log.info('Waiting for system to be less busy, try', tries);
571
589
  await Pledge.after(500);
572
590
  tries++;
573
591
  } while (tries < max_tries && alchemy.isTooBusy());