alchemymvc 1.4.2 → 1.4.3

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.
@@ -107,12 +107,7 @@ Paginate.setMethod(function find(model, criteria) {
107
107
  return;
108
108
  }
109
109
 
110
- let PageInfo = conduit.internal('PageInfo');
111
-
112
- if (PageInfo == null) {
113
- PageInfo = {};
114
- conduit.internal('PageInfo', PageInfo);
115
- }
110
+ let PageInfo = conduit.internal('PageInfo') || {};
116
111
 
117
112
  let entry = {
118
113
  page : page,
@@ -127,6 +122,11 @@ Paginate.setMethod(function find(model, criteria) {
127
122
  }
128
123
 
129
124
  PageInfo[name] = entry;
125
+
126
+ // Store the populated object, not an empty one we mutate afterwards: the
127
+ // renderer's internal store snapshots/transforms the value on set, so a
128
+ // later mutation of a previously-stored object no longer propagates.
129
+ conduit.internal('PageInfo', PageInfo);
130
130
  });
131
131
 
132
132
  return pledge;
@@ -130,6 +130,13 @@ Fallback.setMethod(function storeInUpperDatasource(model, data, options) {
130
130
  // Allow extraneous values (the ones we just added in this function)
131
131
  options.extraneous = true;
132
132
 
133
+ // Store the *converted* data (with the temp values added above), not the
134
+ // original app-side record. Without this the upper datasource caches the
135
+ // raw working values: e.g. a class-typed field is structured-cloned by
136
+ // IndexedDB into a plain, prototype-less object that can't be revived on
137
+ // read. The normal create()/update() flow does this same setConvertedData.
138
+ context.setConvertedData(data);
139
+
133
140
  let promise;
134
141
 
135
142
  if (options.create === false) {
@@ -116,7 +116,7 @@ Plugin.setMethod(async function startPlugin() {
116
116
  *
117
117
  * @author Jelle De Loecker <jelle@elevenways.be>
118
118
  * @since 1.4.0
119
- * @version 1.4.0
119
+ * @version 1.4.3
120
120
  *
121
121
  * @return {boolean}
122
122
  */
@@ -138,31 +138,16 @@ Plugin.setMethod(function loadSettingDefinitions() {
138
138
  // Get/create this plugin's group definition
139
139
  let group = this.getSettingsGroup();
140
140
 
141
- // Get any already-existing settings
142
- let parent = alchemy.system_settings.get('plugins');
143
- let existing = parent.get(this.name);
144
-
145
- if (existing) {
146
- // Remove the existing settings.
147
- // They are not linked properly anyway.
148
- parent.remove(this.name);
149
- }
150
-
151
- // Require the settings.js file now
141
+ // Register the plugin's setting definitions
152
142
  this.useFile(settings_path, {client: false});
153
143
 
154
- // Create the new group settings with default values
155
- let default_group = group.generateValue();
156
-
157
- if (this.default_settings) {
158
- default_group.setDefaultValue(this.default_settings);
159
- }
160
-
161
- if (existing) {
162
- default_group.setValueSilently(existing);
163
- }
164
-
165
- parent.injectSubGroupValue(this.name, default_group);
144
+ // Rebuild this plugin's sub-group from its real definitions, preserving any
145
+ // values already applied to it (config overrides / an orphan created before
146
+ // the plugin loaded) plus the plugin's hard-coded default_settings. This is
147
+ // the same orphan-fix the general reconciler uses
148
+ // (Setting.GroupValue#rebuildSubGroup) - a single source of truth.
149
+ let parent = alchemy.system_settings.get('plugins');
150
+ parent.rebuildSubGroup(this.name, group, this.default_settings);
166
151
 
167
152
  return true;
168
153
  });
@@ -1255,6 +1255,118 @@ GroupValue.setMethod(function injectSubGroupValue(name, value) {
1255
1255
  this[VALUE][name] = value;
1256
1256
  });
1257
1257
 
1258
+ /**
1259
+ * Rebuild a single sub-group value from its real definition, preserving any
1260
+ * values that were already applied to the current (possibly orphaned) node.
1261
+ *
1262
+ * A config-file override that lands before a group's definition is registered
1263
+ * leaves an "orphan" value backed by an ad-hoc, action-less definition (see
1264
+ * `Group#assign`). This rebuilds the node from the real definition so it
1265
+ * regains its full sub-structure and its leaves' actions, then silently
1266
+ * replays the captured values. It fires NO actions - those run once, later,
1267
+ * in the `settings.perform_actions` stage.
1268
+ *
1269
+ * @author Jelle De Loecker <jelle@elevenways.be>
1270
+ * @since 1.4.3
1271
+ * @version 1.4.3
1272
+ *
1273
+ * @param {string} name Child to rebuild
1274
+ * @param {Alchemy.Setting.Group} group_definition The real definition
1275
+ * @param {Object} [hardcoded_defaults] Defaults to re-apply
1276
+ *
1277
+ * @return {Alchemy.Setting.GroupValue}
1278
+ */
1279
+ GroupValue.setMethod(function rebuildSubGroup(name, group_definition, hardcoded_defaults) {
1280
+
1281
+ // Capture whatever value currently sits under `name` (orphan or not).
1282
+ let existing = this.get(name);
1283
+
1284
+ if (existing) {
1285
+ // Detach it so the slot is rebuilt cleanly.
1286
+ this.remove(name);
1287
+ }
1288
+
1289
+ // Materialise a fresh value tree from the REAL definition: recursively
1290
+ // creates every real child with its default and the real `.definition`
1291
+ // (which carries the action / global_variable an orphan stub lacks).
1292
+ let fresh = group_definition.generateValue();
1293
+
1294
+ // Re-apply any hard-coded defaults (e.g. a plugin's `default_settings`).
1295
+ if (hardcoded_defaults) {
1296
+ fresh.setDefaultValue(hardcoded_defaults);
1297
+ }
1298
+
1299
+ // Replay the previously-applied values WITHOUT firing actions. Only a
1300
+ // group value can be replayed onto a group; a scalar sitting where a group
1301
+ // belongs is a malformed override - drop it (and warn) rather than crash.
1302
+ if (existing) {
1303
+ if (existing.is_group) {
1304
+ fresh.setValueSilently(existing);
1305
+ } else {
1306
+ alchemy.distinctProblem(
1307
+ 'setting-scalar-over-group-' + group_definition.setting_id,
1308
+ 'Setting "' + group_definition.setting_id + '" is a group but received a non-group override value; ignoring it'
1309
+ );
1310
+ }
1311
+ }
1312
+
1313
+ // Slot the rebuilt node back into the live tree, in place.
1314
+ this.injectSubGroupValue(name, fresh);
1315
+
1316
+ return fresh;
1317
+ });
1318
+
1319
+ /**
1320
+ * Walk this group's REAL definition tree and rebuild any sub-group whose live
1321
+ * value was fabricated ad-hoc (by a config-file override that arrived before
1322
+ * the definition was registered). Healthy nodes - identified by definition
1323
+ * identity - are left untouched (recursed only, to catch nested orphans), so
1324
+ * this is a strict no-op when there are no orphans. Fires NO actions.
1325
+ *
1326
+ * Intended to run after all setting definitions are registered and before the
1327
+ * database values are applied / actions are fired (the `settings.reconcile`
1328
+ * stage).
1329
+ *
1330
+ * @author Jelle De Loecker <jelle@elevenways.be>
1331
+ * @since 1.4.3
1332
+ * @version 1.4.3
1333
+ */
1334
+ GroupValue.setMethod(function reconcileOrphanGroups() {
1335
+
1336
+ let definition = this.definition;
1337
+
1338
+ if (!definition || !definition.children) {
1339
+ return;
1340
+ }
1341
+
1342
+ for (let [child_name, child_definition] of definition.children) {
1343
+
1344
+ // Only sub-groups can be orphaned. A leaf either already carries the
1345
+ // real definition, or is an unknown key with nothing to rebuild.
1346
+ if (!child_definition.is_group) {
1347
+ continue;
1348
+ }
1349
+
1350
+ let live = this.get(child_name);
1351
+
1352
+ // Nothing applied here yet: nothing to preserve, nothing orphaned.
1353
+ if (!live) {
1354
+ continue;
1355
+ }
1356
+
1357
+ // Healthy node: its value is backed by the canonical definition.
1358
+ // Leave it byte-for-byte, but recurse to catch nested orphans.
1359
+ if (live.is_group && live.definition === child_definition) {
1360
+ live.reconcileOrphanGroups();
1361
+ continue;
1362
+ }
1363
+
1364
+ // Orphan: the live value carries an ad-hoc definition. Rebuild it from
1365
+ // the real definition, replaying the captured values silently.
1366
+ this.rebuildSubGroup(child_name, child_definition);
1367
+ }
1368
+ });
1369
+
1258
1370
  /**
1259
1371
  * Get all the setting values with executable actions in order.
1260
1372
  *
@@ -1429,7 +1541,7 @@ GroupValue.setMethod(function _setPath(silent, path, raw_value) {
1429
1541
  *
1430
1542
  * @author Jelle De Loecker <jelle@elevenways.be>
1431
1543
  * @since 1.4.0
1432
- * @version 1.4.0
1544
+ * @version 1.4.3
1433
1545
  *
1434
1546
  * @param {string|Array} path
1435
1547
  * @param {Value}
@@ -1450,7 +1562,31 @@ GroupValue.setMethod(function forceValueInstanceAtPath(path, value) {
1450
1562
 
1451
1563
  while (path.length && current) {
1452
1564
  let next = path.shift();
1453
- current = current.get(next);
1565
+
1566
+ let child = current.get(next);
1567
+
1568
+ // Create missing intermediate group values instead of crashing.
1569
+ // (A partial config-file override of a group can drop sibling
1570
+ // sub-group value instances; recreate them on demand here.)
1571
+ if (!child) {
1572
+
1573
+ // Can only create a child under a group value; a leaf has no
1574
+ // child definitions to generate from.
1575
+ if (!current.is_group) {
1576
+ return;
1577
+ }
1578
+
1579
+ let child_definition = current.definition.get(next);
1580
+
1581
+ if (!child_definition) {
1582
+ return;
1583
+ }
1584
+
1585
+ child = child_definition.generateValue();
1586
+ current.injectSubGroupValue(next, child);
1587
+ }
1588
+
1589
+ current = child;
1454
1590
  }
1455
1591
 
1456
1592
  current[VALUE][last] = value;
@@ -15,13 +15,32 @@ const settings = STAGES.createStage('settings');
15
15
  // Do not start this stage before the datasources are connected
16
16
  settings.dependsOn('datasource.connect');
17
17
 
18
+ /**
19
+ * "settings.reconcile"
20
+ * Rebuild any setting groups that were orphaned by a config-file override
21
+ * landing before the group's definition was registered (app definitions load
22
+ * in `app_bootstrap`, plugins in `load_app.plugins` - both after the config
23
+ * merge in the Alchemy constructor). Runs before `load` so the database values
24
+ * land on the real-definition value nodes, and before `perform_actions` so
25
+ * actions fire exactly once over the reconciled tree. Silent: fires no actions.
26
+ *
27
+ * @author Jelle De Loecker <jelle@elevenways.be>
28
+ * @since 1.4.3
29
+ * @version 1.4.3
30
+ *
31
+ * @type {Alchemy.Stages.Stage}
32
+ */
33
+ const reconcile = settings.createStage('reconcile', () => {
34
+ alchemy.system_settings.reconcileOrphanGroups();
35
+ });
36
+
18
37
  /**
19
38
  * "settings.load"
20
39
  * Load the settings from the database
21
40
  *
22
41
  * @author Jelle De Loecker <jelle@elevenways.be>
23
42
  * @since 1.4.0
24
- * @version 1.4.0
43
+ * @version 1.4.3
25
44
  *
26
45
  * @type {Alchemy.Stages.Stage}
27
46
  */
@@ -38,6 +57,11 @@ const load = settings.createStage('load', async () => {
38
57
  }
39
58
  });
40
59
 
60
+ // Orphan reconciliation must finish before DB values are applied, so the
61
+ // values land on the real-definition nodes. Creation order already runs
62
+ // `reconcile` first; this makes the contract explicit and reorder-proof.
63
+ load.dependsOn('settings.reconcile');
64
+
41
65
  /**
42
66
  * "settings.perform_actions"
43
67
  * Do all the setting-associated actions.
@@ -204,7 +204,7 @@ const middleware = routes.createStage('middleware', () => {
204
204
  *
205
205
  * @author Jelle De Loecker <jelle@elevenways.be>
206
206
  * @since 1.4.0
207
- * @version 1.4.0
207
+ * @version 1.4.3
208
208
  *
209
209
  * @type {Alchemy.Stages.Stage}
210
210
  */
@@ -212,6 +212,16 @@ const app_routes = routes.createStage('app_routes', () => {
212
212
  try {
213
213
  alchemy.useOnce(libpath.resolve(PATH_APP, 'config', 'routes.js'));
214
214
  } catch (err) {
215
+
216
+ // A missing routes.js is fine (the file is optional). Any other error
217
+ // is a real problem inside the file and must NOT be swallowed - that
218
+ // would leave the app running with no routes and only a warning.
219
+ let is_not_found = err.code === 'ENOENT' || err.message.indexOf('Cannot find') > -1;
220
+
221
+ if (!is_not_found) {
222
+ throw err;
223
+ }
224
+
215
225
  // Only output warning when not in client mode
216
226
  if (!alchemy.getSetting('client_mode')) {
217
227
  log.warn('No app routes were found:', err);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "alchemymvc",
3
3
  "description": "MVC framework for Node.js",
4
- "version": "1.4.2",
4
+ "version": "1.4.3",
5
5
  "author": "Jelle De Loecker <jelle@elevenways.be>",
6
6
  "keywords": [
7
7
  "alchemy",