alchemymvc 1.4.0 → 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 (37) 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 +2 -0
  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_model/00-base_criteria.js +14 -0
  12. package/lib/app/helper_model/05-criteria_expressions.js +30 -7
  13. package/lib/app/helper_model/10-model_criteria.js +47 -8
  14. package/lib/app/helper_model/document.js +11 -2
  15. package/lib/app/helper_model/model.js +6 -3
  16. package/lib/class/conduit.js +5 -2
  17. package/lib/class/controller.js +1 -0
  18. package/lib/class/document.js +39 -11
  19. package/lib/class/import_stream_parser.js +299 -0
  20. package/lib/class/migration.js +5 -2
  21. package/lib/class/model.js +10 -140
  22. package/lib/class/plugin.js +32 -3
  23. package/lib/class/router.js +24 -27
  24. package/lib/class/schema_client.js +38 -7
  25. package/lib/class/sitemap.js +2 -2
  26. package/lib/core/alchemy.js +110 -162
  27. package/lib/core/alchemy_load_functions.js +64 -5
  28. package/lib/core/base.js +2 -2
  29. package/lib/core/middleware.js +31 -5
  30. package/lib/core/setting.js +12 -9
  31. package/lib/scripts/create_constants.js +5 -1
  32. package/lib/stages/00-load_core.js +8 -2
  33. package/lib/testing/browser.js +1164 -0
  34. package/lib/testing/harness.js +840 -0
  35. package/package.json +10 -3
  36. package/testing/browser.js +27 -0
  37. package/testing.js +37 -0
@@ -0,0 +1,299 @@
1
+ const libstream = require('stream');
2
+
3
+ /**
4
+ * The ImportStreamParser class:
5
+ * Handles parsing of Alchemy's binary import stream format.
6
+ *
7
+ * Stream format:
8
+ * - 0x01 [1-byte size] [model name]: Model header
9
+ * - 0x02 [4-byte size BE] [document data]: Document data
10
+ * - 0xFF [4-byte size BE] [extra data]: Extra import data for current document
11
+ *
12
+ * @author Jelle De Loecker <jelle@elevenways.be>
13
+ * @since 1.4.1
14
+ * @version 1.4.1
15
+ */
16
+ const ImportStreamParser = Function.inherits('Alchemy.Base', function ImportStreamParser(input, options) {
17
+
18
+ // The input stream to parse
19
+ this.input = input;
20
+
21
+ // Options
22
+ this.options = options || {};
23
+
24
+ // The model resolver function - receives model name, returns Model instance
25
+ // Can throw an error to abort parsing
26
+ this.model_resolver = null;
27
+
28
+ // State machine variables
29
+ this.current_type = null;
30
+ this.extra_stream = null;
31
+ this.stopped = false;
32
+ this.paused = false;
33
+ this.buffer = null;
34
+ this.model = null;
35
+ this.value = null;
36
+ this.seen = 0;
37
+ this.left = 0;
38
+ this.size = 0;
39
+ this.doc = null;
40
+
41
+ // Track stream end and pending imports
42
+ this.stream_ended = false;
43
+ this.pending_import = false;
44
+
45
+ // The pledge that resolves when parsing is complete
46
+ this.pledge = new Pledge();
47
+ });
48
+
49
+ /**
50
+ * Set the model resolver function.
51
+ * Called when a 0x01 model header is encountered.
52
+ *
53
+ * The resolver receives (model_name, current_model) and should:
54
+ * - Return a Model instance to use for subsequent documents
55
+ * - Throw an error to abort parsing
56
+ *
57
+ * @author Jelle De Loecker <jelle@elevenways.be>
58
+ * @since 1.4.1
59
+ * @version 1.4.1
60
+ *
61
+ * @param {Function} resolver (model_name, current_model) => Model
62
+ */
63
+ ImportStreamParser.setMethod(function setModelResolver(resolver) {
64
+ this.model_resolver = resolver;
65
+ });
66
+
67
+ /**
68
+ * Start parsing the input stream.
69
+ * Returns a Pledge that resolves when parsing is complete.
70
+ *
71
+ * @author Jelle De Loecker <jelle@elevenways.be>
72
+ * @since 1.4.1
73
+ * @version 1.4.1
74
+ *
75
+ * @return {Pledge}
76
+ */
77
+ ImportStreamParser.setMethod(function parse() {
78
+
79
+ let that = this;
80
+
81
+ if (!this.model_resolver) {
82
+ return Pledge.reject(new Error('No model resolver has been set'));
83
+ }
84
+
85
+ this.input.on('data', function onData(data) {
86
+
87
+ if (that.stopped) {
88
+ return;
89
+ }
90
+
91
+ if (that.buffer) {
92
+ that.buffer = Buffer.concat([that.buffer, data]);
93
+ } else {
94
+ that.buffer = data;
95
+ }
96
+
97
+ that.handleBuffer();
98
+ });
99
+
100
+ this.input.on('end', function onEnd() {
101
+ that.stream_ended = true;
102
+
103
+ // Only resolve if we're not in the middle of importing a document
104
+ if (!that.stopped && !that.pending_import) {
105
+ that.pledge.resolve();
106
+ }
107
+ });
108
+
109
+ this.input.on('error', function onError(err) {
110
+ that.stopped = true;
111
+ that.pledge.reject(err);
112
+ });
113
+
114
+ return this.pledge;
115
+ });
116
+
117
+ /**
118
+ * Handle the current buffer data.
119
+ * Implements the state machine for parsing packet headers.
120
+ *
121
+ * State machine:
122
+ * - current_type = null: waiting for a new packet header
123
+ * - current_type = 0x01/0x02/0xFF: header parsed, processing payload
124
+ *
125
+ * We must NOT set current_type until we have the FULL header,
126
+ * otherwise a TCP chunk boundary could leave us in an invalid state.
127
+ *
128
+ * @author Jelle De Loecker <jelle@elevenways.be>
129
+ * @since 1.4.1
130
+ * @version 1.4.1
131
+ */
132
+ ImportStreamParser.setMethod(function handleBuffer() {
133
+
134
+ if (this.paused) {
135
+ return;
136
+ }
137
+
138
+ if (!this.current_type) {
139
+ // Need at least 1 byte to peek at the marker type
140
+ if (this.buffer.length < 1) {
141
+ return;
142
+ }
143
+
144
+ let marker = this.buffer.readUInt8(0);
145
+
146
+ if (marker == 0x01) {
147
+ // Type 0x01: 1-byte marker + 1-byte size = 2 bytes header
148
+ if (this.buffer.length < 2) {
149
+ return; // Wait for more data (don't consume the marker yet)
150
+ }
151
+ this.current_type = marker;
152
+ this.size = this.buffer.readUInt8(1);
153
+ this.buffer = this.buffer.slice(2);
154
+ } else if (marker == 0x02) {
155
+ // Type 0x02: 1-byte marker + 4-byte size = 5 bytes header
156
+ if (this.buffer.length < 5) {
157
+ return; // Wait for more data (don't consume the marker yet)
158
+ }
159
+ this.current_type = marker;
160
+ this.size = this.buffer.readUInt32BE(1);
161
+ this.buffer = this.buffer.slice(5);
162
+ } else if (marker == 0xFF) {
163
+ // Type 0xFF: 1-byte marker + 4-byte size = 5 bytes header
164
+ if (this.buffer.length < 5) {
165
+ return; // Wait for more data (don't consume the marker yet)
166
+ }
167
+ this.current_type = marker;
168
+ this.size = this.buffer.readUInt32BE(1);
169
+ this.buffer = this.buffer.slice(5);
170
+ this.seen = 0;
171
+
172
+ if (!this.doc) {
173
+ this.stopped = true;
174
+ this.pledge.reject(new Error('Found extra import data, but no active document'));
175
+ } else {
176
+ this.extra_stream = new libstream.PassThrough();
177
+ this.doc.extraImportFromStream(this.extra_stream);
178
+ }
179
+ } else {
180
+ // Unknown marker - this shouldn't happen in valid data
181
+ this.stopped = true;
182
+ this.pledge.reject(new Error('Unknown marker byte: 0x' + marker.toString(16)));
183
+ return;
184
+ }
185
+ }
186
+
187
+ this.handlePayload();
188
+ });
189
+
190
+ /**
191
+ * Handle the payload data after a header has been parsed.
192
+ *
193
+ * @author Jelle De Loecker <jelle@elevenways.be>
194
+ * @since 1.4.1
195
+ * @version 1.4.1
196
+ */
197
+ ImportStreamParser.setMethod(function handlePayload() {
198
+
199
+ let that = this;
200
+
201
+ // Handle extra data streaming (0xFF)
202
+ if (this.current_type == 0xFF) {
203
+ this.left = this.size - this.seen;
204
+ this.value = this.buffer.slice(0, this.left);
205
+
206
+ this.seen += this.value.length;
207
+
208
+ if (this.value.length == this.buffer.length) {
209
+ this.buffer = null;
210
+ } else if (this.value.length < this.buffer.length) {
211
+ this.buffer = this.buffer.slice(this.left);
212
+ }
213
+
214
+ this.extra_stream.write(this.value);
215
+
216
+ if (this.value.length == this.left) {
217
+ this.extra_stream.end();
218
+ this.current_type = null;
219
+
220
+ if (this.buffer) {
221
+ this.handleBuffer();
222
+ }
223
+ }
224
+
225
+ return;
226
+ }
227
+
228
+ // Wait for full payload
229
+ if (this.buffer.length >= this.size) {
230
+ this.value = this.buffer.slice(0, this.size);
231
+ this.buffer = this.buffer.slice(this.size);
232
+ } else {
233
+ // Wait for next call
234
+ return;
235
+ }
236
+
237
+ // Handle model header (0x01)
238
+ if (this.current_type == 0x01) {
239
+ let model_name = this.value.toString();
240
+
241
+ try {
242
+ this.model = this.model_resolver(model_name, this.model);
243
+ this.doc = null;
244
+ } catch (err) {
245
+ this.stopped = true;
246
+ return this.pledge.reject(err);
247
+ }
248
+
249
+ if (!this.model) {
250
+ this.stopped = true;
251
+ return this.pledge.reject(new Error('Model resolver returned no model for "' + model_name + '"'));
252
+ }
253
+
254
+ this.current_type = null;
255
+ this.size = 0;
256
+ }
257
+ // Handle document data (0x02)
258
+ else if (this.current_type == 0x02) {
259
+ this.doc = this.model.createDocument();
260
+ this.input.pause();
261
+ this.paused = true;
262
+ this.pending_import = true;
263
+
264
+ this.doc.importFromBuffer(this.value, this.options).done(function done(err, result) {
265
+
266
+ that.pending_import = false;
267
+
268
+ if (err) {
269
+ that.stopped = true;
270
+ return that.pledge.reject(err);
271
+ }
272
+
273
+ that.current_type = null;
274
+ that.paused = false;
275
+
276
+ // Check if there's more data to process
277
+ if (that.buffer && that.buffer.length > 0) {
278
+ that.input.resume();
279
+ that.handleBuffer();
280
+ return;
281
+ }
282
+
283
+ // No more data in buffer
284
+ that.input.resume();
285
+
286
+ // If the stream has ended and there's no more data, resolve
287
+ if (that.stream_ended && !that.stopped) {
288
+ that.pledge.resolve();
289
+ }
290
+ });
291
+
292
+ return;
293
+ }
294
+
295
+ // Continue processing remaining buffer
296
+ if (this.buffer && this.buffer.length) {
297
+ this.handleBuffer();
298
+ }
299
+ });
@@ -20,7 +20,7 @@ const Migration = Function.inherits('Alchemy.Base', function Migration(document)
20
20
  *
21
21
  * @author Jelle De Loecker <jelle@elevenways.be>
22
22
  * @since 1.2.0
23
- * @version 1.2.0
23
+ * @version 1.4.1
24
24
  */
25
25
  Migration.setStatic(async function start() {
26
26
 
@@ -32,7 +32,10 @@ Migration.setStatic(async function start() {
32
32
 
33
33
  await dir.loadContents();
34
34
 
35
- for (let entry of dir) {
35
+ // Sort entries alphabetically so numbered prefixes (001_, 002_) run in order
36
+ let entries = [...dir].sort((a, b) => a.name.localeCompare(b.name));
37
+
38
+ for (let entry of entries) {
36
39
 
37
40
  let name = entry.name.beforeLast('.js');
38
41
 
@@ -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
  *
@@ -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
 
@@ -1053,34 +1058,12 @@ RouterClass.setMethod('delete', function _delete(name, paths, fnc, options) {
1053
1058
  return this.add(['delete'], name, paths, fnc, options);
1054
1059
  });
1055
1060
 
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
1061
  /**
1079
1062
  * Get the full route object, for internal use
1080
1063
  *
1081
1064
  * @author Jelle De Loecker <jelle@elevenways.be>
1082
1065
  * @since 0.5.0
1083
- * @version 0.5.0
1066
+ * @version 1.4.1
1084
1067
  *
1085
1068
  * @param {Object} result Optional object to store sectioned results in
1086
1069
  *
@@ -1111,7 +1094,14 @@ RouterClass.setMethod(function getFullRoutes(result) {
1111
1094
  temp = {};
1112
1095
 
1113
1096
  for (prefix in route.paths) {
1114
- temp[prefix] = mount + route.paths[prefix].source;
1097
+ let source = route.paths[prefix].source;
1098
+
1099
+ // Avoid double slashes when mount is '/' and path starts with '/'
1100
+ if (mount == '/' && source[0] == '/') {
1101
+ temp[prefix] = source;
1102
+ } else {
1103
+ temp[prefix] = mount + source;
1104
+ }
1115
1105
  }
1116
1106
 
1117
1107
  section[route.name] = route;
@@ -1175,7 +1165,7 @@ RouterClass.setMethod(function getOptions(result) {
1175
1165
  *
1176
1166
  * @author Jelle De Loecker <jelle@elevenways.be>
1177
1167
  * @since 0.2.0
1178
- * @version 1.3.21
1168
+ * @version 1.4.1
1179
1169
  *
1180
1170
  * @param {Object} result Optional object to store sectioned results in
1181
1171
  *
@@ -1206,7 +1196,14 @@ RouterClass.setMethod(function getRoutes(result) {
1206
1196
  temp = {};
1207
1197
 
1208
1198
  for (prefix in route.paths) {
1209
- temp[prefix] = mount + route.paths[prefix].source;
1199
+ let source = route.paths[prefix].source;
1200
+
1201
+ // Avoid double slashes when mount is '/' and path starts with '/'
1202
+ if (mount == '/' && source[0] == '/') {
1203
+ temp[prefix] = source;
1204
+ } else {
1205
+ temp[prefix] = mount + source;
1206
+ }
1210
1207
  }
1211
1208
 
1212
1209
  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